use axum::extract::Request;
use axum::http::{StatusCode, header::ORIGIN};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
pub fn origin_is_loopback(origin: &str) -> bool {
let after_scheme = match origin.split_once("://") {
Some((_scheme, rest)) => rest,
None => return false,
};
let authority = after_scheme.split('/').next().unwrap_or("");
let host = if let Some(rest) = authority.strip_prefix('[') {
match rest.split_once(']') {
Some((h, _port)) => h,
None => return false,
}
} else {
authority.split(':').next().unwrap_or("")
};
host == "localhost" || host == "::1" || host.starts_with("127.")
}
pub async fn guard_write_origin(req: Request, next: Next) -> Response {
if !req.method().is_safe()
&& let Some(origin) = req.headers().get(ORIGIN)
{
match origin.to_str() {
Ok(value) if !origin_is_loopback(value) => {
tracing::warn!(
origin = %value,
"console write route rejected cross-origin request (same-origin guard)"
);
return (
StatusCode::FORBIDDEN,
axum::Json(serde_json::json!({
"error": "cross-origin write requests are not allowed",
})),
)
.into_response();
}
Ok(_) => {}
Err(_) => {
tracing::warn!("console write route rejected request with non-UTF-8 Origin header");
return (
StatusCode::FORBIDDEN,
axum::Json(serde_json::json!({
"error": "malformed Origin header",
})),
)
.into_response();
}
}
}
next.run(req).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn origin_is_loopback_accepts_local_hosts() {
assert!(origin_is_loopback("http://127.0.0.1:7788"));
assert!(origin_is_loopback("http://127.0.0.1"));
assert!(origin_is_loopback("http://localhost:7788"));
assert!(origin_is_loopback("http://localhost"));
assert!(origin_is_loopback("http://127.5.6.7:9000"));
assert!(origin_is_loopback("http://[::1]:7788"));
assert!(origin_is_loopback("https://localhost:443"));
}
#[test]
fn origin_is_loopback_rejects_remote_hosts() {
assert!(!origin_is_loopback("http://evil.example.com"));
assert!(!origin_is_loopback("https://evil.example.com:8443"));
assert!(!origin_is_loopback("http://10.0.0.5:7788"));
assert!(!origin_is_loopback("http://100.64.1.2:7788")); assert!(!origin_is_loopback("http://127evil.com")); }
#[test]
fn origin_is_loopback_rejects_malformed() {
assert!(!origin_is_loopback("127.0.0.1:7788")); assert!(!origin_is_loopback(""));
assert!(!origin_is_loopback("garbage"));
}
}