Compare commits

..
1 Commits
Author SHA1 Message Date
jcoffey-dev a742d0cd87 Take a token, never a password, outside DAV
ci / fork-checks (pull_request) Successful in 52s
ci / build (pull_request) Canceled after 11m17s
Anyone could host a copy of a front end on a server of their own,
collect a person's password there, and replay it as HTTP Basic against
JMAP or the API. Cross-origin rules don't stop that, since a server
isn't a browser, and neither does client registration, since Basic
never goes through OAuth (contract C-23).

JMAP (session, API, upload, download, event source, WebSocket), /api,
/auth/introspect, /auth/userinfo and authenticated /auth/register now
refuse an Authorization: Basic header before looking at the password,
with a 401 whose only challenge is Bearer. A wrong password gets the
same answer as the right one. CalDAV and CardDAV keep Basic, and their
401s still offer it. The sign-in page's /api/auth takes the password in
its body and is unaffected, as is the token endpoint's client
authentication.

Bootstrap and recovery mode accept Basic everywhere, as they keep
permissive CORS. INBUXA_HTTP_BASIC_AUTH=all puts it back everywhere;
dav is the default, and any other value logs a warning and keeps it.
Test builds accept Basic everywhere, since the integration suites sign
in with passwords, and legacy_protocols.py sets the variable.

Tested: unit tests for the paths, and tests/e2e/http_basic_auth.py
against the debug build, 26 checks, including both front ends' sign-in
path and a refused unregistered redirect for an ordinary account.
2026-09-29 06:50:23 -07:00
2 changed files with 6 additions and 4 deletions
+3 -1
View File
@@ -277,7 +277,9 @@ Each has an ID, and tests name the IDs they check.
with passwords. Checked by `tests/e2e/http_basic_auth.py` against the debug
build, 26 checks: everything above, both front ends' sign-in path, a wrong
password answered exactly as the right one, and a redirect URI the webmail
didn't register refused.
didn't register refused for an ordinary account. Accounts holding
`OAuthClientOverride` (administrators) skip client and redirect checks on
the sign-in endpoint, as upstream does; that's C-9's gap, not this one's.
Observed before the change, in INBUXA's production logs from 2026-09-20 to 2026-09-29:
every HTTPS password sign-in was the operator's own, apart from
ihasmail-inbuxa's password sign-in on 2026-09-22, before it moved to OAuth.
+3 -3
View File
@@ -185,9 +185,9 @@ def main():
admin, admin_pw = updated["username"], secret_file("basic-admin", updated["secret"])
# After setup, the default: Basic on DAV only. What follows needs a
# tracer to stdout, to read warnings back, and a user account for the
# webmail's password check. Both are made with a token, since Basic no
# longer reaches JMAP.
# tracer to stdout, to read warnings back, and an account without
# administrator rights, which skip client checks (OAuthClientOverride).
# Both are made with a token, since Basic no longer reaches JMAP.
restart()
admin_token, how = token(admin, admin_pw)
if not admin_token: