Cross-origin requests only from the front ends' origins (contract C-14)

This commit is contained in:
2026-09-18 13:44:55 -07:00
parent a45e0ef8b1
commit 8c1879e853
4 changed files with 112 additions and 0 deletions
+15
View File
@@ -62,6 +62,9 @@ pub struct Http {
pub url_https: String,
pub allowed_endpoint: IfBlock,
pub response_headers: Vec<(hyper::header::HeaderName, hyper::header::HeaderValue)>,
/// inbuxa: origins allowed cross-origin access (contract C-14). Empty when
/// CORS is permissive (bootstrap, recovery, or `usePermissiveCors`).
pub cors_origins: Vec<hyper::header::HeaderValue>,
pub use_forwarded: bool,
pub redirect_root: Option<String>,
}
@@ -408,6 +411,17 @@ impl Http {
#[cfg(not(feature = "dev_mode"))]
let use_permissive_cors = http.use_permissive_cors || bp.registry.is_recovery_mode();
// inbuxa: otherwise only the front ends' origins get cross-origin
// access, echoed per request (contract C-14)
let cors_origins = if use_permissive_cors {
Vec::new()
} else {
crate::manager::first_party::front_end_origins()
.into_iter()
.filter_map(|origin| hyper::header::HeaderValue::from_str(&origin).ok())
.collect()
};
if use_permissive_cors {
http_headers.push((
hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN,
@@ -464,6 +478,7 @@ impl Http {
http.rate_limit_anonymous
},
response_headers: http_headers,
cors_origins,
use_forwarded: http.use_x_forwarded,
redirect_root: http.redirect_root,
}
+48
View File
@@ -142,6 +142,43 @@ fn base_url(bp: &Bootstrap, system: &SystemSettings) -> String {
format!("https://{host}")
}
/// The origin (`scheme://host[:port]`) of a front end's address, lowercased,
/// without a default port. `None` if it isn't an `http` or `https` URL.
pub fn origin_of(url: &str) -> Option<String> {
let uri = url.trim().parse::<hyper::Uri>().ok()?;
let scheme = uri.scheme_str()?.to_ascii_lowercase();
let default_port = match scheme.as_str() {
"https" => 443,
"http" => 80,
_ => return None,
};
let host = uri.host()?.to_ascii_lowercase();
if host.is_empty() {
return None;
}
Some(match uri.port_u16() {
Some(port) if port != default_port => format!("{scheme}://{host}:{port}"),
_ => format!("{scheme}://{host}"),
})
}
/// The origins allowed to make cross-origin requests (contract C-14): INBUXA
/// Admin's, the webmail's, and `INBUXA_CORS_EXTRA_ORIGINS` (comma-separated).
///
/// inbuxa: read from the environment until `x:FrontEnds` exists (C-4).
pub fn front_end_origins() -> Vec<String> {
let mut origins = Vec::new();
for url in [env("ADMIN_URL"), env("WEBMAIL_URL")].into_iter().flatten() {
origins.extend(origin_of(&url));
}
if let Some(extra) = env("CORS_EXTRA_ORIGINS") {
origins.extend(extra.split(',').filter_map(origin_of));
}
origins.sort();
origins.dedup();
origins
}
fn env(name: &str) -> Option<String> {
types::branding::env_var(name)
.ok()
@@ -315,6 +352,17 @@ mod tests {
assert_eq!(clients[1].secret.as_deref(), Some("s3cret"));
}
#[test]
fn origins() {
assert_eq!(origin_of("https://Admin.Example.org/"), Some("https://admin.example.org".into()));
assert_eq!(origin_of("https://admin.example.org:443/x"), Some("https://admin.example.org".into()));
assert_eq!(origin_of("http://localhost:5173"), Some("http://localhost:5173".into()));
assert_eq!(origin_of("https://h:8443/app"), Some("https://h:8443".into()));
assert_eq!(origin_of("ftp://h"), None);
assert_eq!(origin_of("not a url"), None);
assert_eq!(origin_of(""), None);
}
#[test]
fn webmail_needs_a_secret() {
let clients = first_party_clients("https://h", &[], Some(" "), Some(("https://w", "")));