feat(server): 业务事件契约——白名单+6事件与枚举校验

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-18 22:10:20 +08:00
parent 4a54087070
commit 3b9d8ecee0
3 changed files with 76 additions and 1 deletions
+35
View File
@@ -8,6 +8,39 @@ from tracker import constants as C
_SENSITIVE = re.compile("|".join(C.SENSITIVE_KEY_PATTERNS), re.I)
_URL_DROP_KEY = re.compile("|".join(C.URL_PARAM_DROP_PATTERNS), re.I)
# 业务事件属性值域(spec §2 枚举/格式表;全部非自由文本)
_BIZ_CHANNEL = {"whatsapp", "phone", "email"}
_BIZ_RESULT = {"mail_sent", "mail_failed"}
_BIZ_ERROR = {"invalid", "spam", "other"}
_SUBMIT_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$|^$") # uuid4,可空
_FORM_RE = re.compile(r"^cf7-\d{1,10}$") # CF7 表单 DOM id(f{n}-p{m} → 服务端规范化为 cf7-{form_id})
_PROD_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$") # slug ≤100
def _biz_props_ok(name, props) -> bool:
"""业务事件属性逐键校验(spec §2 枚举/格式表;全部非自由文本)。
键级白名单已在调用方过完,这里管值:类型 + 枚举 + 格式。"""
def s(k): return isinstance(props.get(k), str)
def form_ok(): return s("form_id") and bool(_FORM_RE.match(props["form_id"]))
def sid_ok():
sid = props.get("submission_id", "")
return isinstance(sid, str) and bool(_SUBMIT_RE.match(sid))
if name == "contact_click":
return s("channel") and props["channel"] in _BIZ_CHANNEL
if name == "form_start":
return form_ok()
if name in ("form_submit", "form_submit_result"):
return form_ok() and sid_ok() and (
name == "form_submit" or (s("result_code") and props["result_code"] in _BIZ_RESULT))
if name == "form_error":
fc = props.get("field_count")
return (form_ok() and sid_ok() and s("error_code") and props["error_code"] in _BIZ_ERROR
and isinstance(fc, int) and 0 <= fc <= 99 and not isinstance(fc, bool))
if name == "product_view":
return (s("product_id") and bool(_PROD_RE.match(props["product_id"]))
and isinstance(props.get("inferred"), bool)
and s("rule_version") and len(props["rule_version"]) <= 16)
return True
def _utf8_ok(s: str) -> bool:
"""孤代理(如 "\ud800")在下游 encode/JSON 序列化时必然 500,入口处即拒。"""
try:
@@ -140,6 +173,8 @@ def validate_event(ev, *, v2=False, batch_site_id="", batch_visitor_id="") -> st
return "sensitive_field"
if not set(props) <= C.EVENT_PROP_WHITELIST.get(ev["event_name"], set()):
return "property_not_allowed"
if ev["event_name"] in C.BIZ_EVENT_NAMES and not _biz_props_ok(ev["event_name"], props):
return "property_not_allowed"
if not depth_ok:
return "properties_depth"
# 体积按 UTF-8 字节数守门(汉字多字节,按字符数会被绕过);孤代理 encode 必炸,按 invalid 拒;