fix(auth): deny /app in multi-user BasicAuth; warn in single-user - #1168
fix(auth): deny /app in multi-user BasicAuth; warn in single-user#1168cbcoutinho wants to merge 1 commit into
Conversation
SessionAuthBackend returned ["authenticated", "admin"] for any caller whenever
OAuth was off — no cookie, no header, no credential. /app is mounted
unconditionally in every deployment mode, so in multi-user BasicAuth anyone who
could reach the port got the admin UI: webhook enable/disable, vector-viz search
over the indexed corpus, and revoke.
The identity it granted was `cfg("NEXTCLOUD_USERNAME", "admin")`, and that mode
*forbids* configuring NEXTCLOUD_USERNAME, so it resolved to a literal "admin" —
a user who need not exist. Nothing legitimate was served by that branch: /app is
a session UI and multi-user BasicAuth has no session, callers authenticate per
request against /mcp instead. So it now fails closed.
Single-user keeps the pass-through. The server holds exactly one identity and
every request already acts as it, so there is nothing to distinguish callers by;
that is the deployment model for a personal instance. What was missing is that
the assumption was undocumented, so startup now warns that /app is
unauthenticated and the port must not be exposed.
Note on the misconfiguration case: multi-user with NEXTCLOUD_USERNAME set
already hard-fails at startup — those vars are in that mode's `forbidden` list
and get_app() raises on any config error. Verified, and now pinned by a test so
it cannot regress silently. The backend deny does not rely on it.
Tests verified to fail against the previous behaviour.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @cbcoutinho's task in 8m 18s —— View job Claude's reviewTodo list
SummaryThe core fix — deny One thing worth verifying before merge, since it isn't covered by the new tests: 🟡 Possible unhandled exception instead of a clean deny (needs live verification)
Every route served under
Starlette resolves Practically the security property still holds (no 200 with admin access), but it's the wrong failure mode — noisy unhandled exceptions/500s instead of a controlled deny. I wasn't able to execute a live reproduction in this review environment (no shell/interpreter execution permission here), so this needs to be verified directly: build the docker compose up --build -d mcp # multi-user-basic profile
curl -i http://localhost:<port>/app/
curl -i http://localhost:<port>/app/webhooksIf either 500s, options include registering an exception handler for 🟢 Test coverage nit
🟢 Minor style note
Nice to have, confirmed correct
|
|



Closes Deck #853. Third item from the multi-user security audit, after #1150 and #1161.
The problem
SessionAuthBackend.authenticate()returned["authenticated", "admin"]for any caller whenever OAuth was off — no cookie, no header, no credential of any kind:/appis mounted unconditionally in every deployment mode (theMount("/app", …)sits at top-level indentation inget_app(), not inside a mode conditional), so in multi-user BasicAuth anyone who could reach the port got the admin UI: webhook enable/disable, vector-viz search over the whole indexed corpus, and revoke.Worse, the identity it granted was
cfg("NEXTCLOUD_USERNAME", "admin")— and that mode forbids configuringNEXTCLOUD_USERNAME, so it resolved to a literal"admin", a user who need not exist.The fix is mode-aware, because the two modes are genuinely different
Multi-user BasicAuth → deny.
/appis a session UI and this mode has no session concept; callers authenticate per request with their own credentials against/mcp. Nothing legitimate was served by that branch — it only ever showed a static, forbidden-to-configure identity — so it now fails closed.Single-user BasicAuth → unchanged, plus a startup warning. The server holds exactly one identity and every request already acts as it, so there is nothing to distinguish callers by. That is the deployment model for a personal instance, not a bug. What was missing is that the assumption was undocumented, so startup now warns:
A blanket denial would have broken working personal deployments for no security gain, which is why this isn't one gate.
On the misconfiguration hard-fail
Multi-user with
NEXTCLOUD_USERNAMEset already hard-fails at startup — I verified rather than assumed:nextcloud_username/nextcloud_passwordare in that mode'sforbiddenlist (config_validators.py:97-100)errors, andget_app()doesraise ValueError(error_msg)(app.py:1555)MCP_DEPLOYMENT_MODEwins over auto-detection, so the mode really is multi-user when setConfirmed live:
validate_configurationreturns bothForbidden configuration: NEXTCLOUD_USERNAME/NEXTCLOUD_PASSWORDerrors. That behaviour is now pinned by a test so it can't regress silently — and the backend's deny deliberately does not depend on it (test_multi_user_basic_denies_even_with_username_configured).Test coverage
tests/unit/test_app_ui_basicauth_access.py— 4 tests. The two deny tests are verified to fail against the previous behaviour, so they pin the fix rather than restate it.2786 unit tests pass under a CI-equivalent clean environment;
ruff,ruff format,ty, secrets scan green.Breaking change
Not flagged. Multi-user BasicAuth
/appaccess is removed, but it was never functional in that mode — it served a static identity the mode forbids configuring. No/mcpbehaviour changes, and single-user is untouched.🤖 Generated with Claude Code
This PR was generated with the help of AI, and reviewed by a Human