0.34.6_repo_cleanup
This commit is contained in:
1 parent
ce1a90397c
commit
133d2f0f91
273 files changed
+5969
-6926
No files matched your search
+111
-66
@@ -1,9 +1,5 @@
|
||||
// Only log browser origin metadata: never cookies, CSRF tokens, Authorization,
|
||||
// query strings, or untrusted forwarding data. Malformed origins are redacted.
|
||||
fn diagnostic_admin_authority(value: &str) -> &str {
|
||||
if value.len() <= 255 && !value.contains('@')
|
||||
&& value.parse::<http::uri::Authority>().is_ok()
|
||||
{
|
||||
if value.len() <= 255 && !value.contains('@') && value.parse::<http::uri::Authority>().is_ok() {
|
||||
value
|
||||
} else {
|
||||
"(invalid)"
|
||||
@@ -47,7 +43,11 @@ fn diagnostic_forwarded_header<'a>(headers: &'a HeaderMap, name: &str, trusted:
|
||||
return "(invalid)";
|
||||
};
|
||||
if name == "x-forwarded-proto" {
|
||||
if matches!(value, "http" | "https") { value } else { "(invalid)" }
|
||||
if matches!(value, "http" | "https") {
|
||||
value
|
||||
} else {
|
||||
"(invalid)"
|
||||
}
|
||||
} else {
|
||||
diagnostic_admin_authority(value)
|
||||
}
|
||||
@@ -63,14 +63,19 @@ fn invalid_admin_origin(
|
||||
) -> Response {
|
||||
let headers = request.headers();
|
||||
let path = request.uri().path();
|
||||
let path = if path.len() <= 160 { path } else { "(long_path)" };
|
||||
let path = if path.len() <= 160 {
|
||||
path
|
||||
} else {
|
||||
"(long_path)"
|
||||
};
|
||||
let browser_host = single_header(headers, HOST.as_str())
|
||||
.map(diagnostic_admin_authority)
|
||||
.unwrap_or("(absent_or_duplicate)");
|
||||
let browser_fetch_site = match single_header(headers, "sec-fetch-site") {
|
||||
None => "(absent)",
|
||||
Some("same-origin" | "same-site" | "cross-site" | "none") =>
|
||||
single_header(headers, "sec-fetch-site").unwrap_or("(invalid)"),
|
||||
Some("same-origin" | "same-site" | "cross-site" | "none") => {
|
||||
single_header(headers, "sec-fetch-site").unwrap_or("(invalid)")
|
||||
}
|
||||
_ => "(invalid)",
|
||||
};
|
||||
tracing::warn!(
|
||||
@@ -88,12 +93,15 @@ fn invalid_admin_origin(
|
||||
forwarded_proto = diagnostic_forwarded_header(headers, "x-forwarded-proto", trusted_proto),
|
||||
"administration origin rejected"
|
||||
);
|
||||
// When a browser navigates to the dashboard document, JSON here would
|
||||
// replace the entire page with a raw API error. Keep the 403 (no shell or
|
||||
// protected data) but present a standalone recovery message instead.
|
||||
let wants_html = request.headers().get(http::header::ACCEPT)
|
||||
let wants_html = request
|
||||
.headers()
|
||||
.get(http::header::ACCEPT)
|
||||
.and_then(|header| header.to_str().ok())
|
||||
.is_some_and(|value| value.split(',').any(|entry| entry.trim_start().starts_with("text/html")));
|
||||
.is_some_and(|value| {
|
||||
value
|
||||
.split(',')
|
||||
.any(|entry| entry.trim_start().starts_with("text/html"))
|
||||
});
|
||||
if request.method() == Method::GET && panel_document_path(request.uri().path()) && wants_html {
|
||||
return secure((
|
||||
StatusCode::FORBIDDEN,
|
||||
@@ -117,10 +125,8 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
||||
let headers = request.headers();
|
||||
let trusted_host = trusted_admin_proxy(&state, peer);
|
||||
let trusted_proto = trusted_admin_forwarded_proto(&state, peer);
|
||||
// Pinned browser origin is panel-only. Certificate automation using API
|
||||
// credentials keeps the original forwarding/TLS authorization semantics.
|
||||
let automation_credentials = headers.contains_key(CERTIFICATE_API_KEY_HEADER)
|
||||
|| headers.contains_key(AUTHORIZATION);
|
||||
let automation_credentials =
|
||||
headers.contains_key(CERTIFICATE_API_KEY_HEADER) || headers.contains_key(AUTHORIZATION);
|
||||
let pinned_authority_result = if automation_credentials {
|
||||
Ok(None)
|
||||
} else {
|
||||
@@ -128,25 +134,42 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
||||
};
|
||||
let pinned_authority = match pinned_authority_result {
|
||||
Ok(value) => value,
|
||||
Err(()) => return invalid_admin_origin(&request, "pinned_origin_mismatch", Some("https"),
|
||||
state.active_administration.public_origin.as_deref().and_then(|value| value.strip_prefix("https://")),
|
||||
trusted_host, trusted_proto),
|
||||
Err(()) => {
|
||||
return invalid_admin_origin(
|
||||
&request,
|
||||
"pinned_origin_mismatch",
|
||||
Some("https"),
|
||||
state
|
||||
.active_administration
|
||||
.public_origin
|
||||
.as_deref()
|
||||
.and_then(|value| value.strip_prefix("https://")),
|
||||
trusted_host,
|
||||
trusted_proto,
|
||||
)
|
||||
}
|
||||
};
|
||||
let (scheme, external_authority) = if let Some(authority) = pinned_authority {
|
||||
// HTTPS is a trusted, administrator-pinned identity here. Do not select
|
||||
// any list element of an ambiguous forwarded-proto chain.
|
||||
("https", authority)
|
||||
} else {
|
||||
let scheme = match external_admin_scheme(&state, headers, peer, transport) {
|
||||
Ok(value) => value,
|
||||
Err(()) => {
|
||||
return invalid_admin_origin(&request, "external_scheme", None, None, trusted_host, trusted_proto);
|
||||
return invalid_admin_origin(
|
||||
&request,
|
||||
"external_scheme",
|
||||
None,
|
||||
None,
|
||||
trusted_host,
|
||||
trusted_proto,
|
||||
);
|
||||
}
|
||||
};
|
||||
let authority = match external_admin_authority(&state, headers, peer, &raw_host, scheme == "https") {
|
||||
Ok(value) => value,
|
||||
Err(()) => return secure(reject(StatusCode::FORBIDDEN, "auth.invalid_host")),
|
||||
};
|
||||
let authority =
|
||||
match external_admin_authority(&state, headers, peer, &raw_host, scheme == "https") {
|
||||
Ok(value) => value,
|
||||
Err(()) => return secure(reject(StatusCode::FORBIDDEN, "auth.invalid_host")),
|
||||
};
|
||||
(scheme, authority)
|
||||
};
|
||||
if headers.get_all(ORIGIN).iter().count() > 1
|
||||
@@ -156,13 +179,27 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
||||
.is_none_or(|origin| !admin_origin_matches(origin, scheme, &external_authority))
|
||||
})
|
||||
{
|
||||
return invalid_admin_origin(&request, "origin_mismatch", Some(scheme), Some(&external_authority), trusted_host, trusted_proto);
|
||||
return invalid_admin_origin(
|
||||
&request,
|
||||
"origin_mismatch",
|
||||
Some(scheme),
|
||||
Some(&external_authority),
|
||||
trusted_host,
|
||||
trusted_proto,
|
||||
);
|
||||
}
|
||||
if headers
|
||||
.get("sec-fetch-site")
|
||||
.is_some_and(|h| h == "cross-site")
|
||||
{
|
||||
return invalid_admin_origin(&request, "cross_site_fetch", Some(scheme), Some(&external_authority), trusted_host, trusted_proto);
|
||||
return invalid_admin_origin(
|
||||
&request,
|
||||
"cross_site_fetch",
|
||||
Some(scheme),
|
||||
Some(&external_authority),
|
||||
trusted_host,
|
||||
trusted_proto,
|
||||
);
|
||||
}
|
||||
let path = request.uri().path().to_owned();
|
||||
let public = anonymous_allowed(request.method(), &path);
|
||||
@@ -174,7 +211,10 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
||||
let mut certificate_api_access = None;
|
||||
let certificate_api_session = if headers.contains_key(CERTIFICATE_API_KEY_HEADER) {
|
||||
let Some(operation) = certificate_api_operation.as_ref() else {
|
||||
return secure(reject(StatusCode::FORBIDDEN, "certificate_api.invalid_route"));
|
||||
return secure(reject(
|
||||
StatusCode::FORBIDDEN,
|
||||
"certificate_api.invalid_route",
|
||||
));
|
||||
};
|
||||
match certificate_api_credentials(headers) {
|
||||
Ok(None) => None,
|
||||
@@ -243,35 +283,47 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let general_api_session = if certificate_api_session.is_none() && headers.contains_key(AUTHORIZATION) {
|
||||
// No cookies are accepted as a fallback when a malformed Bearer header is supplied.
|
||||
let bearer = single_header(headers, AUTHORIZATION.as_str())
|
||||
.and_then(|header| header.strip_prefix("Bearer "));
|
||||
let Some(token) = bearer.filter(|value| valid_certificate_api_token(value)) else {
|
||||
return secure(reject(StatusCode::UNAUTHORIZED, "api_token.invalid_token"));
|
||||
let general_api_session =
|
||||
if certificate_api_session.is_none() && headers.contains_key(AUTHORIZATION) {
|
||||
let bearer = single_header(headers, AUTHORIZATION.as_str())
|
||||
.and_then(|header| header.strip_prefix("Bearer "));
|
||||
let Some(token) = bearer.filter(|value| valid_certificate_api_token(value)) else {
|
||||
return secure(reject(StatusCode::UNAUTHORIZED, "api_token.invalid_token"));
|
||||
};
|
||||
let Some(peer) = peer else {
|
||||
return secure(reject(StatusCode::FORBIDDEN, "api_token.peer_required"));
|
||||
};
|
||||
if !peer.0.ip().is_loopback() && scheme != "https" {
|
||||
return secure(reject(StatusCode::FORBIDDEN, "api_token.https_required"));
|
||||
}
|
||||
let hash = auth::secret_hash(token);
|
||||
let peer_ip = crate::vhost::canonical_ip(peer.0.ip());
|
||||
let grant = match state.store.verify_api_token(&hash, peer_ip).await {
|
||||
Ok(Some(grant)) => grant,
|
||||
Ok(None) => {
|
||||
return secure(reject(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"api_token.invalid_token_or_ip",
|
||||
))
|
||||
}
|
||||
Err(error) => return secure(auth::internal(error).into_response()),
|
||||
};
|
||||
if !general_api_token_allowed(request.method(), &path, &grant.permission) {
|
||||
return secure(reject(
|
||||
StatusCode::FORBIDDEN,
|
||||
"api_token.insufficient_permission",
|
||||
));
|
||||
}
|
||||
Some(Session {
|
||||
token_hash: hash,
|
||||
csrf: String::new(),
|
||||
username: format!("api-token:{}", grant.id),
|
||||
expires_at: auth::now().saturating_add(60),
|
||||
generation: 0,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let Some(peer) = peer else {
|
||||
return secure(reject(StatusCode::FORBIDDEN, "api_token.peer_required"));
|
||||
};
|
||||
if !peer.0.ip().is_loopback() && scheme != "https" {
|
||||
return secure(reject(StatusCode::FORBIDDEN, "api_token.https_required"));
|
||||
}
|
||||
let hash = auth::secret_hash(token);
|
||||
let peer_ip = crate::vhost::canonical_ip(peer.0.ip());
|
||||
let grant = match state.store.verify_api_token(&hash, peer_ip).await {
|
||||
Ok(Some(grant)) => grant,
|
||||
Ok(None) => return secure(reject(StatusCode::UNAUTHORIZED, "api_token.invalid_token_or_ip")),
|
||||
Err(error) => return secure(auth::internal(error).into_response()),
|
||||
};
|
||||
if !general_api_token_allowed(request.method(), &path, &grant.permission) {
|
||||
return secure(reject(StatusCode::FORBIDDEN, "api_token.insufficient_permission"));
|
||||
}
|
||||
Some(Session {
|
||||
token_hash: hash, csrf: String::new(),
|
||||
username: format!("api-token:{}", grant.id),
|
||||
expires_at: auth::now().saturating_add(60), generation: 0,
|
||||
})
|
||||
} else { None };
|
||||
let external_credential = certificate_api_session.is_some() || general_api_session.is_some();
|
||||
let session_seconds = state
|
||||
.config
|
||||
@@ -280,9 +332,6 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
||||
.config
|
||||
.administration
|
||||
.session_seconds();
|
||||
// Panel document routes optionally authenticate so a direct refresh can choose
|
||||
// the application shell when the browser still has a valid cookie. Public static
|
||||
// login resources and capability discovery never load operational state.
|
||||
let session = if certificate_api_session.is_some() {
|
||||
certificate_api_session
|
||||
} else if general_api_session.is_some() {
|
||||
@@ -352,16 +401,12 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
||||
}
|
||||
}
|
||||
if matches!(path.as_str(), "/docs" | "/docs/") && response.status().is_success() {
|
||||
// Swagger UI sets inline layout styles. This exception is limited to the
|
||||
// authenticated documentation document; scripts remain self-only/no-eval.
|
||||
response.headers_mut().insert("content-security-policy", HeaderValue::from_static(
|
||||
"default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; font-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"));
|
||||
}
|
||||
response
|
||||
}
|
||||
async fn auth_status(State(state): State<Arc<App>>) -> Result<Json<Value>, ApiError> {
|
||||
// The one bit needed to render first-account setup; no version, paths, names,
|
||||
// listeners, provider state, metrics or configuration before authentication.
|
||||
Ok(Json(
|
||||
json!({"setup_required":state.store.admin_record().await.map_err(auth::internal)?.is_none()}),
|
||||
))
|
||||
|
||||
Reference in new issue
Block a user