fix(server): v2入口加固——page_id孤代理/NaN区间值拒绝,共享ISO解析
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone as dt_tz
|
||||
from urllib.parse import parse_qsl, urlencode, urlparse
|
||||
from django.utils import timezone
|
||||
from tracker import constants as C
|
||||
@@ -16,6 +16,16 @@ def _utf8_ok(s: str) -> bool:
|
||||
except UnicodeEncodeError:
|
||||
return False
|
||||
|
||||
|
||||
def parse_iso_dt(v):
|
||||
"""datetime 原样返回;ISO 字符串解析为 aware datetime("Z"→+00:00,naive 视为 UTC
|
||||
——Django≥5 已删除 django.utils.timezone.utc)。解析失败抛 ValueError/TypeError,
|
||||
错误策略由调用方自定:collect 视图整批 400(invalid_occurred_at),ingest v2 逐事件拒。"""
|
||||
if not isinstance(v, str):
|
||||
return v
|
||||
d = datetime.fromisoformat(v.replace("Z", "+00:00"))
|
||||
return d if d.tzinfo else d.replace(tzinfo=dt_tz.utc)
|
||||
|
||||
def sanitize_url(url: str) -> str:
|
||||
"""三轨制清洗(spec §2.4):黑名单参数名删 + 归因白名单提 + 其余按值判别。
|
||||
另:userinfo 剥离、fragment 删除。规则版本 SANITIZE_RULE_VERSION。"""
|
||||
@@ -96,9 +106,15 @@ def validate_event(ev, *, v2=False, batch_site_id="", batch_visitor_id="") -> st
|
||||
or not isinstance(ev.get("session_id"), str) or not ev["session_id"]):
|
||||
return "attribution_mismatch"
|
||||
pid = ev.get("page_id")
|
||||
if ev["event_name"] != "session_resume" and (
|
||||
not isinstance(pid, str) or not pid or len(pid) > 64 or not _utf8_ok(pid)):
|
||||
return "invalid_page_id"
|
||||
if isinstance(pid, str) and pid:
|
||||
# 非空 page_id 全事件统一完整校验——session_resume 也不豁免:合规 JSON 可携带
|
||||
# 孤代理(json.loads 接受 "\ud800"),漏检 _utf8_ok 会在存储时炸整批事务
|
||||
if len(pid) > 64 or not _utf8_ok(pid):
|
||||
return "invalid_page_id"
|
||||
elif pid not in ("", None):
|
||||
return "invalid_page_id" # 非 str 类型(123/[] 等)
|
||||
elif ev["event_name"] != "session_resume":
|
||||
return "invalid_page_id" # 空/缺失:仅 session_resume 豁免(归属会话而非页面)
|
||||
eid = ev.get("event_id")
|
||||
if not isinstance(eid, str) or not eid or len(eid) > 64 or not _utf8_ok(eid):
|
||||
return "invalid_event_id"
|
||||
@@ -126,10 +142,13 @@ def validate_event(ev, *, v2=False, batch_site_id="", batch_visitor_id="") -> st
|
||||
return "property_not_allowed"
|
||||
if not depth_ok:
|
||||
return "properties_depth"
|
||||
# 体积按 UTF-8 字节数守门(汉字多字节,按字符数会被绕过);孤代理 encode 必炸,按 invalid 拒
|
||||
# 体积按 UTF-8 字节数守门(汉字多字节,按字符数会被绕过);孤代理 encode 必炸,按 invalid 拒;
|
||||
# NaN/Infinity 同拒(allow_nan=False)——json.loads 默认收这两个字面量,落库时 SQLite 的
|
||||
# JSON 有效性校验拒 + bulk_create(ignore_conflicts) 静默丢行,而 accepted 已计数(幽灵事件)
|
||||
try:
|
||||
prop_bytes = len(json.dumps(props, ensure_ascii=False, default=str).encode("utf-8"))
|
||||
except UnicodeEncodeError:
|
||||
prop_bytes = len(json.dumps(props, ensure_ascii=False, default=str,
|
||||
allow_nan=False).encode("utf-8"))
|
||||
except (UnicodeEncodeError, ValueError):
|
||||
return "properties_invalid"
|
||||
if prop_bytes > C.MAX_PROP_JSON_BYTES:
|
||||
return "properties_too_large"
|
||||
|
||||
Reference in New Issue
Block a user