fix: build dry-run log entirely from constant labels
CodeQL still traced taint through the previous helpers: the endpoint regex fallback returned source-derived text and the payload dict keys flowed verbatim into the log. Resolve both by sourcing every logged token from literal lookup tables — endpoints map to constant path labels and payload keys map through a field-name allow-list, with unknown keys/paths collapsing to <field>/<other>. No source-derived string (key, value or id) reaches the logger.
This commit is contained in:
@@ -34,6 +34,13 @@ _SQUAD_BULK_TEMPLATES = {
|
|||||||
"add-users": "/internal-squads/<id>/bulk-actions/add-users",
|
"add-users": "/internal-squads/<id>/bulk-actions/add-users",
|
||||||
"remove-users": "/internal-squads/<id>/bulk-actions/remove-users",
|
"remove-users": "/internal-squads/<id>/bulk-actions/remove-users",
|
||||||
}
|
}
|
||||||
|
# Exact intercepted endpoints that carry no id and are safe to log verbatim.
|
||||||
|
# Mapped to themselves so the logged value comes from this literal table, not
|
||||||
|
# from the (tainted) request endpoint.
|
||||||
|
_SAFE_LITERAL_ENDPOINTS = {
|
||||||
|
"/users": "/users",
|
||||||
|
"/hwid/devices/delete": "/hwid/devices/delete",
|
||||||
|
}
|
||||||
|
|
||||||
# Panel payloads can carry proxy credentials (e.g. trojanPassword, ssPassword,
|
# Panel payloads can carry proxy credentials (e.g. trojanPassword, ssPassword,
|
||||||
# vless/vmess uuids) and PII (email, telegramId). Redact such values before they
|
# vless/vmess uuids) and PII (email, telegramId). Redact such values before they
|
||||||
@@ -43,9 +50,35 @@ _SENSITIVE_KEY_RE = re.compile(
|
|||||||
r"email|mail|phone|telegram|mnemonic",
|
r"email|mail|phone|telegram|mnemonic",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
# Mask opaque id-like path segments (UUIDs, long tokens) in logged endpoints.
|
# Field names the dry-run validator understands. Keys are echoed into the log
|
||||||
_ENDPOINT_ID_RE = re.compile(r"(?<=/)(?:[0-9a-fA-F]{8}-[0-9a-fA-F-]{8,}|[A-Za-z0-9_-]{24,})")
|
# only via this table (value == name), so the logged key is always a literal and
|
||||||
|
# never the raw, source-derived dict key. Unknown keys collapse to "<field>".
|
||||||
|
_FIELD_LABELS = {
|
||||||
|
name: name
|
||||||
|
for name in (
|
||||||
|
"uuid",
|
||||||
|
"username",
|
||||||
|
"status",
|
||||||
|
"expireAt",
|
||||||
|
"trafficLimitBytes",
|
||||||
|
"trafficLimitStrategy",
|
||||||
|
"hwidDeviceLimit",
|
||||||
|
"telegramId",
|
||||||
|
"email",
|
||||||
|
"description",
|
||||||
|
"tag",
|
||||||
|
"activeInternalSquads",
|
||||||
|
"activeUserInbounds",
|
||||||
|
"externalSquadUuid",
|
||||||
|
"userUuid",
|
||||||
|
"userUuids",
|
||||||
|
"users",
|
||||||
|
"hwid",
|
||||||
|
)
|
||||||
|
}
|
||||||
_REDACTED = "***"
|
_REDACTED = "***"
|
||||||
|
_UNKNOWN_FIELD = "<field>"
|
||||||
|
_UNKNOWN_ENDPOINT = "<other>"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -115,12 +148,12 @@ class PanelDryRunApiService(PanelApiService):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _safe_endpoint(endpoint: str) -> str:
|
def _safe_endpoint(endpoint: str) -> str:
|
||||||
"""Rebuild the logged path from constant templates so no id leaks.
|
"""Map the request path to a constant log label.
|
||||||
|
|
||||||
Intercepted endpoints are a known, finite set. Each is mapped to a literal
|
Every return value comes from a literal template/table, never from the
|
||||||
template; the raw endpoint (which may embed a user/squad UUID) is never
|
(tainted) endpoint itself, so user/squad UUIDs and any other id-like
|
||||||
echoed into the log, only matched against. Unknown paths fall back to a
|
segment can never reach the log as clear text. The raw path is only
|
||||||
regex mask of id-like segments.
|
matched against, not echoed.
|
||||||
"""
|
"""
|
||||||
raw = str(endpoint or "")
|
raw = str(endpoint or "")
|
||||||
if match := _USER_ACTION_RE.match(raw):
|
if match := _USER_ACTION_RE.match(raw):
|
||||||
@@ -129,13 +162,14 @@ class PanelDryRunApiService(PanelApiService):
|
|||||||
return _SQUAD_BULK_TEMPLATES.get(
|
return _SQUAD_BULK_TEMPLATES.get(
|
||||||
match.group("action"), "/internal-squads/<id>/bulk-actions/<action>"
|
match.group("action"), "/internal-squads/<id>/bulk-actions/<action>"
|
||||||
)
|
)
|
||||||
if raw == "/users":
|
literal = _SAFE_LITERAL_ENDPOINTS.get(raw)
|
||||||
return "/users"
|
if literal is not None:
|
||||||
|
return literal
|
||||||
if raw.startswith("/users/"):
|
if raw.startswith("/users/"):
|
||||||
return "/users/<id>"
|
return "/users/<id>"
|
||||||
if raw.startswith("/internal-squads/"):
|
if raw.startswith("/internal-squads/"):
|
||||||
return "/internal-squads/<id>"
|
return "/internal-squads/<id>"
|
||||||
return _ENDPOINT_ID_RE.sub("<id>", raw)
|
return _UNKNOWN_ENDPOINT
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _summarize_leaf(value: Any) -> Any:
|
def _summarize_leaf(value: Any) -> Any:
|
||||||
@@ -157,19 +191,32 @@ class PanelDryRunApiService(PanelApiService):
|
|||||||
return "<str>"
|
return "<str>"
|
||||||
return f"<{type(value).__name__}>"
|
return f"<{type(value).__name__}>"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _safe_key(key: Any) -> str:
|
||||||
|
"""Return a constant label for a payload key.
|
||||||
|
|
||||||
|
Known field names are echoed from the ``_FIELD_LABELS`` table (the value,
|
||||||
|
not the source-derived key); anything else collapses to ``<field>``. This
|
||||||
|
keeps the raw dict key out of the log entirely.
|
||||||
|
"""
|
||||||
|
if isinstance(key, str):
|
||||||
|
return _FIELD_LABELS.get(key, _UNKNOWN_FIELD)
|
||||||
|
return _UNKNOWN_FIELD
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _redact(cls, value: Any, _depth: int = 0) -> Any:
|
def _redact(cls, value: Any, _depth: int = 0) -> Any:
|
||||||
"""Recursively summarize values, keeping only keys and JSON shape.
|
"""Recursively summarize values, keeping only the JSON shape.
|
||||||
|
|
||||||
Sensitive keys collapse to a placeholder and every scalar leaf is replaced
|
Keys are replaced by constant labels, sensitive keys collapse to a
|
||||||
by a type token, so the resulting structure shows which fields a mutation
|
placeholder, and every scalar leaf becomes a type token. The result shows
|
||||||
would touch without ever logging a field value.
|
which fields a mutation would touch without logging any source-derived
|
||||||
|
string (key or value).
|
||||||
"""
|
"""
|
||||||
if _depth > 6:
|
if _depth > 6:
|
||||||
return "..."
|
return "..."
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
return {
|
return {
|
||||||
str(k): (
|
cls._safe_key(k): (
|
||||||
_REDACTED
|
_REDACTED
|
||||||
if isinstance(k, str) and _SENSITIVE_KEY_RE.search(k)
|
if isinstance(k, str) and _SENSITIVE_KEY_RE.search(k)
|
||||||
else cls._redact(v, _depth + 1)
|
else cls._redact(v, _depth + 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user