From 0032c1804bf85d1f63e72c0f43587852b036bc32 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Mon, 1 Jun 2026 22:48:21 +0300 Subject: [PATCH] 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. --- .../bot/services/panel_dry_run_api_service.py | 67 +++++++++++++++++-- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/backend/bot/services/panel_dry_run_api_service.py b/backend/bot/services/panel_dry_run_api_service.py index 2ea019e..2c19d0d 100644 --- a/backend/bot/services/panel_dry_run_api_service.py +++ b/backend/bot/services/panel_dry_run_api_service.py @@ -22,6 +22,19 @@ _INTERNAL_SQUAD_BULK_RE = re.compile( _LIVE_POST_ENDPOINTS = frozenset({"/system/tools/happ/encrypt"}) _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//actions/enable", + "disable": "/users//actions/disable", + "reset-traffic": "/users//actions/reset-traffic", +} +_SQUAD_BULK_TEMPLATES = { + "add-users": "/internal-squads//bulk-actions/add-users", + "remove-users": "/internal-squads//bulk-actions/remove-users", +} + # Panel payloads can carry proxy credentials (e.g. trojanPassword, ssPassword, # 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. @@ -102,17 +115,61 @@ class PanelDryRunApiService(PanelApiService): @staticmethod def _safe_endpoint(endpoint: str) -> str: - """Mask opaque id-like segments so logged paths carry no private ids.""" - return _ENDPOINT_ID_RE.sub("", str(endpoint or "")) + """Rebuild the logged path from constant templates so no id leaks. + + 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//actions/") + if match := _INTERNAL_SQUAD_BULK_RE.match(raw): + return _SQUAD_BULK_TEMPLATES.get( + match.group("action"), "/internal-squads//bulk-actions/" + ) + if raw == "/users": + return "/users" + if raw.startswith("/users/"): + return "/users/" + if raw.startswith("/internal-squads/"): + return "/internal-squads/" + return _ENDPOINT_ID_RE.sub("", 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 "" + if isinstance(value, int): + return "" + if isinstance(value, float): + return "" + if isinstance(value, str): + return "" + return f"<{type(value).__name__}>" @classmethod 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: return "..." if isinstance(value, dict): return { - k: ( + str(k): ( _REDACTED if isinstance(k, str) and _SENSITIVE_KEY_RE.search(k) else cls._redact(v, _depth + 1) @@ -121,7 +178,7 @@ class PanelDryRunApiService(PanelApiService): } if isinstance(value, (list, tuple)): return [cls._redact(item, _depth + 1) for item in value] - return value + return cls._summarize_leaf(value) @classmethod def _payload_preview(cls, payload: Any) -> str: