v0.31.3
This commit is contained in:
1 parent
0facfe6f5c
commit
95c55da2ee
39 files changed
+715
-151
No files matched your search
+112
-3
@@ -1,3 +1,108 @@
|
||||
// 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()
|
||||
{
|
||||
value
|
||||
} else {
|
||||
"(invalid)"
|
||||
}
|
||||
}
|
||||
|
||||
fn diagnostic_admin_origin(headers: &HeaderMap) -> &str {
|
||||
if !headers.contains_key(ORIGIN) {
|
||||
return "(absent)";
|
||||
}
|
||||
let Some(value) = single_header(headers, ORIGIN.as_str()) else {
|
||||
return "(invalid)";
|
||||
};
|
||||
if value == "null" {
|
||||
return "null";
|
||||
}
|
||||
if value.len() > 255 || value.contains('@') || value.contains('#') {
|
||||
return "(invalid)";
|
||||
}
|
||||
let Ok(uri) = value.parse::<http::Uri>() else {
|
||||
return "(invalid)";
|
||||
};
|
||||
if !matches!(uri.scheme_str(), Some("http" | "https"))
|
||||
|| uri.query().is_some()
|
||||
|| !matches!(uri.path(), "" | "/")
|
||||
|| uri.authority().is_none()
|
||||
{
|
||||
return "(invalid)";
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
fn diagnostic_forwarded_header<'a>(headers: &'a HeaderMap, name: &str, trusted: bool) -> &'a str {
|
||||
if !headers.contains_key(name) {
|
||||
return "(absent)";
|
||||
}
|
||||
if !trusted {
|
||||
return "(untrusted)";
|
||||
}
|
||||
let Some(value) = single_header(headers, name) else {
|
||||
return "(invalid)";
|
||||
};
|
||||
if name == "x-forwarded-proto" {
|
||||
if matches!(value, "http" | "https") { value } else { "(invalid)" }
|
||||
} else {
|
||||
diagnostic_admin_authority(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_admin_origin(
|
||||
request: &Request,
|
||||
reason: &'static str,
|
||||
scheme: Option<&str>,
|
||||
authority: Option<&str>,
|
||||
trusted_host: bool,
|
||||
trusted_proto: bool,
|
||||
) -> Response {
|
||||
let headers = request.headers();
|
||||
let path = request.uri().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)"),
|
||||
_ => "(invalid)",
|
||||
};
|
||||
tracing::warn!(
|
||||
reason,
|
||||
method = %request.method(),
|
||||
path,
|
||||
browser_origin = diagnostic_admin_origin(headers),
|
||||
browser_host,
|
||||
expected_scheme = scheme.unwrap_or("(unknown)"),
|
||||
expected_authority = authority.map(diagnostic_admin_authority).unwrap_or("(unknown)"),
|
||||
browser_fetch_site,
|
||||
trusted_forwarded_host = trusted_host,
|
||||
trusted_forwarded_proto = trusted_proto,
|
||||
forwarded_host = diagnostic_forwarded_header(headers, "x-forwarded-host", trusted_host),
|
||||
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)
|
||||
.and_then(|header| header.to_str().ok())
|
||||
.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,
|
||||
axum::response::Html("<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>RustProxy - Access blocked</title><h1>RustProxy - Could not open the panel</h1><p>The request origin was rejected. Check the proxy/URL and try again.</p><p><a href=\"\">Reload panel</a></p></html>"),
|
||||
).into_response());
|
||||
}
|
||||
secure(reject(StatusCode::FORBIDDEN, "auth.invalid_origin"))
|
||||
}
|
||||
|
||||
async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next) -> Response {
|
||||
let transport = request
|
||||
.extensions()
|
||||
@@ -10,9 +115,13 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
||||
Err(()) => return secure(reject(StatusCode::FORBIDDEN, "auth.invalid_host")),
|
||||
};
|
||||
let headers = request.headers();
|
||||
let trusted_host = trusted_admin_proxy(&state, peer);
|
||||
let trusted_proto = trusted_admin_forwarded_proto(&state, peer);
|
||||
let scheme = match external_admin_scheme(&state, headers, peer, transport) {
|
||||
Ok(value) => value,
|
||||
Err(()) => return secure(reject(StatusCode::FORBIDDEN, "auth.invalid_origin")),
|
||||
Err(()) => {
|
||||
return invalid_admin_origin(&request, "external_scheme", None, None, trusted_host, trusted_proto);
|
||||
}
|
||||
};
|
||||
let external_authority =
|
||||
match external_admin_authority(&state, headers, peer, &raw_host, scheme == "https") {
|
||||
@@ -26,13 +135,13 @@ 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 secure(reject(StatusCode::FORBIDDEN, "auth.invalid_origin"));
|
||||
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 secure(reject(StatusCode::FORBIDDEN, "auth.invalid_origin"));
|
||||
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);
|
||||
|
||||
Reference in new issue
Block a user