fix: drop clear-text values and ids from panel dry-run logs
Rebuild logged endpoints from constant path templates and reduce every payload leaf to a JSON type token, so user/squad UUIDs and PII (email, telegramId) can never reach the dry-run log as clear text. Resolves CodeQL py/clear-text-logging-sensitive-data findings.
This commit is contained in:
@@ -22,6 +22,19 @@ _INTERNAL_SQUAD_BULK_RE = re.compile(
|
|||||||
_LIVE_POST_ENDPOINTS = frozenset({"/system/tools/happ/encrypt"})
|
_LIVE_POST_ENDPOINTS = frozenset({"/system/tools/happ/encrypt"})
|
||||||
_KNOWN_TRAFFIC_STRATEGIES = frozenset({"NO_RESET", "DAY", "WEEK", "MONTH"})
|
_KNOWN_TRAFFIC_STRATEGIES = frozenset({"NO_RESET", "DAY", "WEEK", "MONTH"})
|
||||||
|
|
||||||
|
# Constant path templates for intercepted endpoints. The logged path is rebuilt
|
||||||
|
# from these literals (never from the raw endpoint) so user/squad UUIDs and any
|
||||||
|
# other id-like segment can never reach the log as clear text.
|
||||||
|
_USER_ACTION_TEMPLATES = {
|
||||||
|
"enable": "/users/<id>/actions/enable",
|
||||||
|
"disable": "/users/<id>/actions/disable",
|
||||||
|
"reset-traffic": "/users/<id>/actions/reset-traffic",
|
||||||
|
}
|
||||||
|
_SQUAD_BULK_TEMPLATES = {
|
||||||
|
"add-users": "/internal-squads/<id>/bulk-actions/add-users",
|
||||||
|
"remove-users": "/internal-squads/<id>/bulk-actions/remove-users",
|
||||||
|
}
|
||||||
|
|
||||||
# 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
|
||||||
# reach the dry-run log so secrets are never written in clear text.
|
# reach the dry-run log so secrets are never written in clear text.
|
||||||
@@ -102,17 +115,61 @@ class PanelDryRunApiService(PanelApiService):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _safe_endpoint(endpoint: str) -> str:
|
def _safe_endpoint(endpoint: str) -> str:
|
||||||
"""Mask opaque id-like segments so logged paths carry no private ids."""
|
"""Rebuild the logged path from constant templates so no id leaks.
|
||||||
return _ENDPOINT_ID_RE.sub("<id>", str(endpoint or ""))
|
|
||||||
|
Intercepted endpoints are a known, finite set. Each is mapped to a literal
|
||||||
|
template; the raw endpoint (which may embed a user/squad UUID) is never
|
||||||
|
echoed into the log, only matched against. Unknown paths fall back to a
|
||||||
|
regex mask of id-like segments.
|
||||||
|
"""
|
||||||
|
raw = str(endpoint or "")
|
||||||
|
if match := _USER_ACTION_RE.match(raw):
|
||||||
|
return _USER_ACTION_TEMPLATES.get(match.group("action"), "/users/<id>/actions/<action>")
|
||||||
|
if match := _INTERNAL_SQUAD_BULK_RE.match(raw):
|
||||||
|
return _SQUAD_BULK_TEMPLATES.get(
|
||||||
|
match.group("action"), "/internal-squads/<id>/bulk-actions/<action>"
|
||||||
|
)
|
||||||
|
if raw == "/users":
|
||||||
|
return "/users"
|
||||||
|
if raw.startswith("/users/"):
|
||||||
|
return "/users/<id>"
|
||||||
|
if raw.startswith("/internal-squads/"):
|
||||||
|
return "/internal-squads/<id>"
|
||||||
|
return _ENDPOINT_ID_RE.sub("<id>", raw)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _summarize_leaf(value: Any) -> Any:
|
||||||
|
"""Reduce a scalar to a non-sensitive type token.
|
||||||
|
|
||||||
|
Leaf values can carry PII or proxy credentials, so the log never echoes
|
||||||
|
them — only their JSON type. ``None`` is kept so absent fields stay
|
||||||
|
distinguishable from present ones.
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return "<bool>"
|
||||||
|
if isinstance(value, int):
|
||||||
|
return "<int>"
|
||||||
|
if isinstance(value, float):
|
||||||
|
return "<float>"
|
||||||
|
if isinstance(value, str):
|
||||||
|
return "<str>"
|
||||||
|
return f"<{type(value).__name__}>"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _redact(cls, value: Any, _depth: int = 0) -> Any:
|
def _redact(cls, value: Any, _depth: int = 0) -> Any:
|
||||||
"""Recursively replace values under sensitive keys with a placeholder."""
|
"""Recursively summarize values, keeping only keys and JSON shape.
|
||||||
|
|
||||||
|
Sensitive keys collapse to a placeholder and every scalar leaf is replaced
|
||||||
|
by a type token, so the resulting structure shows which fields a mutation
|
||||||
|
would touch without ever logging a field value.
|
||||||
|
"""
|
||||||
if _depth > 6:
|
if _depth > 6:
|
||||||
return "..."
|
return "..."
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
return {
|
return {
|
||||||
k: (
|
str(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)
|
||||||
@@ -121,7 +178,7 @@ class PanelDryRunApiService(PanelApiService):
|
|||||||
}
|
}
|
||||||
if isinstance(value, (list, tuple)):
|
if isinstance(value, (list, tuple)):
|
||||||
return [cls._redact(item, _depth + 1) for item in value]
|
return [cls._redact(item, _depth + 1) for item in value]
|
||||||
return value
|
return cls._summarize_leaf(value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _payload_preview(cls, payload: Any) -> str:
|
def _payload_preview(cls, payload: Any) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user