use crate::utils::{aliases::AEngine, runique_log::get_log};
use axum::{
body::Body,
extract::State,
http::{Request, StatusCode, header},
middleware::Next,
response::{IntoResponse, Response},
};
pub async fn open_redirect_middleware(
State(engine): State<AEngine>,
req: Request<Body>,
next: Next,
) -> Response {
let response = next.run(req).await;
if !response.status().is_redirection() {
return response;
}
let Some(location) = response.headers().get(header::LOCATION) else {
return response;
};
let Ok(location_str) = location.to_str() else {
return response;
};
if is_safe_redirect(location_str, &engine) {
return response;
}
if let Some(level) = get_log().host_validation {
crate::runique_log!(level, location = %location_str, "open redirect blocked");
}
(StatusCode::BAD_REQUEST, "Forbidden redirect").into_response()
}
fn is_safe_redirect(location: &str, engine: &crate::engine::RuniqueEngine) -> bool {
let normalized = location.replace('\\', "/");
let location = normalized.as_str();
if location.starts_with('/') && !location.starts_with("//") {
return true;
}
let host = extract_host(location);
let Some(host) = host else {
return false;
};
if is_local_host(host) {
return true;
}
engine.security_hosts.is_host_allowed(host)
}
pub fn extract_host(location: &str) -> Option<&str> {
let without_scheme = if let Some(rest) = location.strip_prefix("//") {
rest
} else {
location
.strip_prefix("http://")
.or_else(|| location.strip_prefix("https://"))?
};
let host = without_scheme
.split(['/', '?', '#'])
.next()
.filter(|h| !h.is_empty())?;
Some(host)
}
pub fn is_local_host(host: &str) -> bool {
if host.starts_with('[') {
let addr = host.split(']').next().map(|s| &s[1..]).unwrap_or(host);
return is_loopback_ipv6(addr);
}
let bare = host.split(':').next().unwrap_or(host);
bare == "localhost" || bare.starts_with("127.")
}
fn is_loopback_ipv6(addr: &str) -> bool {
if addr == "::1" {
return true;
}
if addr == "0:0:0:0:0:0:0:1" {
return true;
}
if let Some(rest) = addr.to_ascii_lowercase().strip_prefix("::ffff:") {
if rest.starts_with("127.") {
return true;
}
if rest.starts_with("7f") {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_host() {
assert_eq!(extract_host("https://evil.com/path"), Some("evil.com"));
assert_eq!(
extract_host("http://evil.com:8080/path"),
Some("evil.com:8080")
);
assert_eq!(extract_host("//evil.com/path"), Some("evil.com"));
assert_eq!(extract_host("/relative"), None);
assert_eq!(extract_host("https://evil.com"), Some("evil.com"));
assert_eq!(extract_host("https://evil.com?q=1"), Some("evil.com"));
}
#[test]
fn test_is_local_host() {
assert!(is_local_host("localhost"));
assert!(is_local_host("localhost:8080"));
assert!(!is_local_host("notlocalhost.com"));
assert!(!is_local_host("localhost.evil.com"));
assert!(is_local_host("127.0.0.1"));
assert!(is_local_host("127.0.0.1:3000"));
assert!(is_local_host("127.0.0.2"));
assert!(is_local_host("127.1.2.3"));
assert!(is_local_host("127.255.255.255"));
assert!(!is_local_host("128.0.0.1"));
assert!(is_local_host("[::1]"));
assert!(is_local_host("[::1]:8080"));
assert!(is_local_host("[0:0:0:0:0:0:0:1]"));
assert!(is_local_host("[0:0:0:0:0:0:0:1]:443"));
assert!(is_local_host("[::ffff:127.0.0.1]"));
assert!(is_local_host("[::ffff:7f00:1]"));
assert!(is_local_host("[::FFFF:127.0.0.1]"));
assert!(!is_local_host("evil.com"));
assert!(!is_local_host("[::2]"));
}
}