## Security/Privacy Review — Sprint 6G-A.3 **Verdict: FAIL — 2 Important release blockers** ### Findings 1. **Important — Browser CSRF bootstrap is treated as authentication** - **Files:** - `scripts/health/health_dashboard_server.py:799-832` - `scripts/health/health_dashboard_server.py:864-905` - `scripts/health/assets/health-assets/dashboard-v5.js:20-30` - `/health-dashboard-v5` is served without authenticating a user. Every visitor receives page-visible bootstrap material plus its matching cookie, and `/api/v1/browser-session` converts that pair into an authenticated `health_api_session`. - Same-origin, `SameSite=Strict`, CSP, and CSRF checks prevent cross-site abuse, but they do **not** establish an authorized principal. - This becomes more consequential in A.3 because that session now grants access to unreviewed extracted text and original files. - The default loopback binding reduces exposure, but the server is configurable through `HEALTH_DASHBOARD_HOST`/allowed hosts and no enforced or documented authenticated upstream boundary is present. - **Required:** protect the dashboard/bootstrap with an existing authenticated principal, or enforce and document a non-bypassable deployment access boundary. Add a test proving an unauthorized page visitor cannot mint an API session. 2. **Important — Nutrition deep-link key is emitted without proving an exact unique open queue match** - **File:** `scripts/health/dashboard_v5/read_api.py:1428-1462` - Every unmapped item gets `nutrition_queue_key(normalized_name)` solely from its displayed item name. The implementation never queries `nutrition_review_queue` to prove an exact, unique, open match. - `nutrition_review_queue` is queried independently at `read_api.py:1498-1533`, so an unmatched, closed, malformed, or collision-equivalent item can receive a URL key and “Zuordnung prüfen” action that does not identify a valid queue card. - This violates the fail-closed opaque queue identity contract and places a deterministic name-derived token in browser history. - **Required:** derive the key only after an exact open-queue lookup; emit it only for exactly one valid match. Ambiguous or absent matches must return `null`. Add unmatched, closed, and collision/ambiguity tests. ### Checks that passed - Pending-document originals use component-by-component `openat`/`dir_fd`, `O_NOFOLLOW`, `fstat`, regular-file/size and MIME-magic validation. - GET and HEAD independently reopen/revalidate; streaming remains descriptor-pinned against path replacement. - Original responses use generic filenames and privacy headers; tested payloads do not expose stored filenames or paths. - Extracted preview is a separate bounded endpoint; reviewed detail, matches, and current-state FTS remain reviewed-only. - Preview rendering uses text nodes rather than HTML injection. - Nutrition detail does not expose raw JSON, source product/item IDs, database IDs, paths, credentials, or provider secrets. - Day/nutrition requests use generation checks and abort handling sufficient to prevent stale DOM commits. - No V4, `public/`, `skills/`, or configuration paths were changed. ### Verification - Focused tests executed: `PYTHONDONTWRITEBYTECODE=1 pytest -q tests/test_dashboard_v5_sprint6g_a3.py tests/test_dashboard_v5_sprint6e1_original.py tests/test_dashboard_v5_sprint6e4b.py` - Result: **9 passed** - `git diff --check`: **passed** - Review was read-only; **no files created or modified**.