mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 02:05:22 -04:00
Deep-scrub secrets from public settings
/api/auth/settings is auth-exempt (the frontend + the pre-login page read it for keybinds/TTS prefs), so non-admin and unauthenticated callers get a scrubbed copy. The previous scrub only blanked TOP-LEVEL string values whose key matched a short suffix list — so a secret nested under a non-secret parent key, or stored under a key outside the list, would leak. A real exposure when the app is reachable over a Cloudflare tunnel / reverse proxy. - src/settings_scrub.py: NEW stdlib-only module with the scrub helpers (deep/ recursive; broadened secret-key patterns). Kept separate from auth_routes so it imports + unit-tests WITHOUT pulling the FastAPI / auth / database chain (addresses review: the test no longer fails at collection on the DB import). - routes/auth_routes.py: import scrub_settings from the module. - tests/test_settings_scrub.py: import the tiny module directly. Ran: pytest tests/test_settings_scrub.py (8 passed); verified the test pulls no db/auth modules into sys.modules; py_compile routes/auth_routes.py. Co-authored-by: Kanaru92 <107661007+Kanaru92@users.noreply.github.com>
This commit is contained in:
+2
-24
@@ -8,6 +8,7 @@ import os
|
||||
|
||||
from core.auth import AuthManager
|
||||
from src.rate_limiter import RateLimiter
|
||||
from src.settings_scrub import scrub_settings
|
||||
from src.settings import (
|
||||
load_settings as _load_settings,
|
||||
save_settings as _save_settings,
|
||||
@@ -371,29 +372,6 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
||||
|
||||
# ---- App settings (admin-managed) ----
|
||||
|
||||
_SECRET_KEY_PATTERNS = ("_api_key", "_password", "_secret", "_token", "_key")
|
||||
|
||||
def _is_secret_key(name: str) -> bool:
|
||||
n = (name or "").lower()
|
||||
if n in ("google_pse_cx",): # public identifier, not a secret
|
||||
return False
|
||||
return any(n.endswith(p) or n == p.lstrip("_") for p in _SECRET_KEY_PATTERNS)
|
||||
|
||||
def _scrub_settings(settings: dict) -> dict:
|
||||
"""Return a copy of settings with secret-shaped values masked.
|
||||
|
||||
Frontend reads /settings without auth for things like keybinds + TTS
|
||||
prefs. Secrets (search-provider keys, IMAP/SMTP passwords) must NOT
|
||||
be exposed to non-admin callers.
|
||||
"""
|
||||
scrubbed = {}
|
||||
for k, v in (settings or {}).items():
|
||||
if _is_secret_key(k) and isinstance(v, str) and v:
|
||||
scrubbed[k] = "" # presence preserved, value blanked
|
||||
else:
|
||||
scrubbed[k] = v
|
||||
return scrubbed
|
||||
|
||||
@router.get("/settings")
|
||||
async def get_settings(request: Request):
|
||||
"""Returns app settings. Admins get the full set; non-admins get
|
||||
@@ -403,7 +381,7 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
||||
settings = _load_settings()
|
||||
if user and auth_manager.is_admin(user):
|
||||
return settings
|
||||
return _scrub_settings(settings)
|
||||
return scrub_settings(settings)
|
||||
|
||||
@router.post("/settings")
|
||||
async def set_settings(request: Request):
|
||||
|
||||
Reference in New Issue
Block a user