Files
felhom.eu/documentation/audits/proxyauth-spike-2026-10-18/DESIGN-NOTE.md
T

4.7 KiB

R-1100 — one sign-in for the dashboard and FileBrowser: the spike (2026-10-18 evening)

Measured on scratch 9202 (demo-hp, Tier 0): FileBrowser Quantum gtstef/filebrowser:1.5.6-stable (source read at tag v1.5.6-stable, commit 5d9b4df2), Traefik v3.7.13, controller 0.331.0-rc.1. Nothing was built; every change on 9202 was removed (03-teardown.txt). Evidence: 01-strip.txt (every request and answer), 02-teardown-users.txt.

What FileBrowser's proxy auth is (source, at the pinned tag)

  • The config key is auth.methods.proxy (backend/common/settings/auth.go:58, json proxy — not proxyAuth, as the row said), with enabled and header (auth.go:75-78: „FileBrowser blindly accepts the header value as username").
  • Any request that carries the header is that user (backend/http/middleware.go:302-307 on login, :384-401 on every API call, :494-520 getProxyUser). A name FileBrowser has never seen is created on the spot (backend/http/auth.go:81-106), with the user defaults. A name that exists as a password user is refused (auth.go:107-108, ErrWrongLoginMethod) — so the household's admin cannot be reached by a header; a header user is a separate FileBrowser user.

What was measured

Request Answer
a route with no middleware, forged X-Felhom-User: spike-stranger 200 — the drive's listing; FileBrowser created the user
Traefik forwardAuth that answers 200 and sets NO user header, authResponseHeaders: [X-Felhom-User]; forged header (also lower-case, also twice) 401 from FileBrowser („no token present") — Traefik removed the client's header
forwardAuth that sets X-Felhom-User: household-sso only for a valid cookie; cookie ok + forged attacker 200 as household-sso (FileBrowser's own request log names the user) — the auth answer replaced the forged value
the same, wrong cookie + forged header 401 from the auth step; FileBrowser never reached
from inside the Nextcloud container, straight to http://filebrowser:80, forged header 200 — no Traefik on that path

So: Traefik can pass the header only for a valid session and strips one a client sends — proven. But the header is also trusted from any container that reaches FileBrowser's port, and every app sits on traefik-public with it.

The three things a build must solve

  1. The session cannot be read on files.<domain>. The dashboard cookie felhom_session has no Domain (controller/internal/web/auth.go:197-205), so a browser never sends it to files.<domain>, and a forward-auth there sees nothing. Do not widen it to Domain=<domain>: every app's subdomain — third-party code — would then receive the dashboard session. Instead a files-only ticket: „Fájlok ↗" goes through the dashboard, which mints a one-time, ~60-second token; files.<domain>/_felhom/sso?t=… (a Traefik route to the controller) swaps it for a host-only, HttpOnly, Secure cookie for files.<domain>; the forward-auth checks that cookie and answers the header.
  2. Any app container can forge the header (measured). Two cheap closes, either enough, both better: (a) the header name is a per-box secret (X-Felhom-User-<32 hex>), written only into FileBrowser's config and Traefik's dynamic file — an app that does not know the name cannot send it; (b) FileBrowser off traefik-public: Traefik joins felhom-filebrowser instead (and OnlyOffice too, for its server.internalUrl, R-1018). (b) changes the R-1126 gate's shape; (a) does not.
  3. Header users are new FileBrowser users. The household's files permissions for that user must be set by the box (the user defaults decide today); shares the household made as admin stay admin's; logout needs logoutRedirectUrl to the dashboard.

Unchanged by it

  • The password reveal (R-513) stays: admin remains a password user — the fallback when the ticket path fails, and the operator's. The „Fájlok ↗" button would no longer need it.
  • The R-1126 gate keeps checking admin's default login; header users do not touch it.

Cost and recommendation

Controller only (no agent, no hub): a ticket endpoint and a forward-auth endpoint, one Traefik route + middleware, the FileBrowser config block with a secret header name, the header user's permissions, tests for each refusal (forged header via Traefik; forged header from an app container — with a secret name; stale/replayed ticket; wrong host). About one release. Recommendation: build it with (1) the ticket cookie and (2a) the secret header name; do not ship header trust without 2a — the measured app-container path would hand every app the household's files.