From 2df7e942b2e0d3a7b2983d56fccede8c9be72690 Mon Sep 17 00:00:00 2001 From: GeJiaXiang <353358601@qq.com> Date: Mon, 29 Jun 2026 16:41:46 +0800 Subject: [PATCH] fix: require auth for /api/status, add unauthenticated /api/health probe /api/status was exempt from the AUTH_TOKEN middleware so the Docker healthcheck could reach it, but it leaks running_count, binary_version and profiles_total without authentication. Scanners (e.g. fofa) use it to fingerprint exposed instances. Move /api/status behind auth (same as /api/profiles) and add a dedicated /api/health probe that returns only {"status": "ok"} with no system details. Point the Dockerfile healthcheck at /api/health and update the exempt set, README and tests accordingly. --- Dockerfile | 2 +- README.md | 2 +- backend/main.py | 15 +++++++++++++-- backend/tests/test_auth.py | 12 +++++++++++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 962144df..6f4bd108 100644 --- a/Dockerfile +++ b/Dockerfile @@ -57,7 +57,7 @@ RUN python -c "from cloakbrowser.download import ensure_binary; ensure_binary()" EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/status')" || exit 1 + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/health')" || exit 1 VOLUME /data diff --git a/README.md b/README.md index e413c555..f10d8b56 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ When `AUTH_TOKEN` is set: - The web UI shows a login page. Enter the token to unlock. - API consumers pass the token via `Authorization: Bearer ` header. - VNC WebSocket connections are authenticated via the login cookie. -- The `/api/status` endpoint remains unauthenticated (for Docker healthcheck). +- The `/api/health` endpoint remains unauthenticated (for Docker healthcheck); it exposes no system details. The `/api/status` endpoint (running counts, version) now requires authentication. > **Note**: The auth token is transmitted in cleartext over HTTP. If you expose the Manager to the internet, put it behind a reverse proxy with HTTPS (Caddy, nginx, Traefik). diff --git a/backend/main.py b/backend/main.py index 92727a53..6ec6f5e5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -47,11 +47,12 @@ # Optional authentication via AUTH_TOKEN env var. # If not set, all routes are open (local dev). If set, all /api/* routes -# (except /api/auth/* and /api/status) require Bearer token or cookie. +# (except /api/auth/status, /api/auth/login and /api/health) require Bearer +# token or cookie. AUTH_TOKEN: str | None = os.environ.get("AUTH_TOKEN") or None # Paths that bypass authentication even when AUTH_TOKEN is set -_AUTH_EXEMPT = frozenset({"/api/auth/status", "/api/auth/login", "/api/status"}) +_AUTH_EXEMPT = frozenset({"/api/auth/status", "/api/auth/login", "/api/health"}) def _check_auth(scope: Scope) -> bool: @@ -567,6 +568,16 @@ async def get_profile_status(profile_id: str): # ── System Status ───────────────────────────────────────────────────────────── +@app.get("/api/health") +async def health_check(): + """Unauthenticated liveness probe for the Docker healthcheck. + + Intentionally returns no system details; see /api/status (auth-gated) + for running counts and versions. + """ + return {"status": "ok"} + + @app.get("/api/status", response_model=StatusResponse) async def get_system_status(): from cloakbrowser.config import CHROMIUM_VERSION diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index 2dd6f124..5baeaed4 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -128,8 +128,18 @@ def test_logout_clears_cookie(client_auth: TestClient): def test_healthcheck_always_accessible(client_auth: TestClient): - """GET /api/status must work without auth (Docker healthcheck).""" + """GET /api/health must work without auth (Docker healthcheck).""" + resp = client_auth.get("/api/health") + assert resp.status_code == 200 + + +def test_status_requires_auth(client_auth: TestClient): + """GET /api/status must require auth (leaks profile count and version).""" resp = client_auth.get("/api/status") + assert resp.status_code == 401 + resp = client_auth.get( + "/api/status", headers={"Authorization": "Bearer test-secret"} + ) assert resp.status_code == 200