Take a token, never a password, outside DAV
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.
This commit is contained in:
@@ -72,6 +72,10 @@ pub struct Http {
|
||||
pub cors_origins: Vec<hyper::header::HeaderValue>,
|
||||
pub use_forwarded: bool,
|
||||
pub redirect_root: Option<String>,
|
||||
/// inbuxa: HTTP Basic accepted on every endpoint, not only DAV (contract
|
||||
/// C-23). True in bootstrap and recovery mode, or with
|
||||
/// `INBUXA_HTTP_BASIC_AUTH=all`.
|
||||
pub basic_auth_everywhere: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -453,6 +457,35 @@ impl Http {
|
||||
.collect()
|
||||
};
|
||||
|
||||
// inbuxa: outside DAV, HTTP sign-in is a token unless the operator
|
||||
// says otherwise (contract C-23). The integration suites sign in with
|
||||
// passwords over JMAP and the API, so test builds accept Basic
|
||||
// everywhere.
|
||||
#[cfg(feature = "test_mode")]
|
||||
let basic_auth_everywhere = true;
|
||||
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
let basic_auth_everywhere = bp.registry.is_recovery_mode()
|
||||
|| bp.registry.is_bootstrap_mode()
|
||||
|| match types::branding::env_var("HTTP_BASIC_AUTH") {
|
||||
Ok(value) if value.trim().eq_ignore_ascii_case("all") => true,
|
||||
Ok(value)
|
||||
if value.trim().is_empty() || value.trim().eq_ignore_ascii_case("dav") =>
|
||||
{
|
||||
false
|
||||
}
|
||||
Ok(value) => {
|
||||
bp.build_warning(
|
||||
ObjectType::Http.singleton(),
|
||||
format!(
|
||||
"INBUXA_HTTP_BASIC_AUTH is {value:?}; expected \"dav\" or \"all\". Basic authentication stays on DAV only."
|
||||
),
|
||||
);
|
||||
false
|
||||
}
|
||||
Err(_) => false,
|
||||
};
|
||||
|
||||
if use_permissive_cors {
|
||||
http_headers.push((
|
||||
hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
@@ -512,6 +545,7 @@ impl Http {
|
||||
cors_origins,
|
||||
use_forwarded: http.use_x_forwarded,
|
||||
redirect_root: http.redirect_root,
|
||||
basic_auth_everywhere,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user