use http::{header, HeaderValue};
use pingora_error::{Error, ErrorType::InvalidHTTPHeader, Result};
use pingora_http::RequestHeader;
pub use pingora_http::authority::{
is_http_family_scheme, raw_target_authority, RawTargetAuthority,
};
pub fn validate_request_authority(req: &RequestHeader) -> Result<()> {
validate_request_authority_fields(req)?;
let host = req.headers.get(header::HOST);
if req.method == http::Method::CONNECT {
return validate_connect_authority(req.raw_path(), host);
}
match raw_target_authority(req.raw_path()) {
RawTargetAuthority::None => {}
RawTargetAuthority::AmbiguousAuthority => {
return Error::e_explain(
InvalidHTTPHeader,
"ambiguous HTTP absolute-form request target",
);
}
RawTargetAuthority::Absolute {
scheme, authority, ..
} => {
if is_http_family_scheme(scheme) {
http::uri::Authority::try_from(authority).map_err(|cause| {
Error::because(
InvalidHTTPHeader,
"unparseable absolute-form authority",
cause,
)
})?;
}
if authority.contains(&b'@') {
return Error::e_explain(InvalidHTTPHeader, "userinfo in request target");
}
if host.is_some_and(|host| host.as_bytes() != authority) {
return Error::e_explain(
InvalidHTTPHeader,
"Host header differs from request-target authority",
);
}
}
}
Ok(())
}
pub(super) fn validate_request_authority_fields(req: &RequestHeader) -> Result<()> {
let mut hosts = req.headers.get_all(header::HOST).iter();
let (host, duplicate_host) = (hosts.next(), hosts.next());
if duplicate_host.is_some() {
return Error::e_explain(InvalidHTTPHeader, "multiple Host header fields");
}
if host.is_some_and(|host| host.as_bytes().contains(&b'@')) {
return Error::e_explain(InvalidHTTPHeader, "userinfo in Host header");
}
let uri_authority = req.uri.authority().map(|authority| authority.as_str());
if uri_authority.is_some_and(|authority| authority.contains('@')) {
return Error::e_explain(InvalidHTTPHeader, "userinfo in URI authority");
}
if host
.zip(uri_authority)
.is_some_and(|(host, authority)| host.as_bytes() != authority.as_bytes())
{
return Error::e_explain(InvalidHTTPHeader, "Host header differs from URI authority");
}
Ok(())
}
fn validate_connect_authority(target: &[u8], host: Option<&HeaderValue>) -> Result<()> {
let authority_end = target
.iter()
.position(|byte| matches!(byte, b'/' | b'?' | b'#'))
.unwrap_or(target.len());
let authority_target = &target[..authority_end];
if authority_target.contains(&b'@') {
return Error::e_explain(InvalidHTTPHeader, "userinfo in request target");
}
if has_ambiguous_port_suffix(authority_target) {
return Error::e_explain(InvalidHTTPHeader, "ambiguous CONNECT request target");
}
let parsed_authority = http::uri::Authority::try_from(authority_target);
if let Ok(authority) = &parsed_authority {
if authority.port().is_some() {
let Some(host) = host else {
return Ok(());
};
return reconcile_connect_host(authority_target, authority.host().as_bytes(), host);
}
}
match raw_target_authority(target) {
RawTargetAuthority::Absolute { authority, .. } => {
if authority.contains(&b'@') {
return Error::e_explain(InvalidHTTPHeader, "userinfo in request target");
}
if has_ambiguous_port_suffix(authority) {
return Error::e_explain(InvalidHTTPHeader, "ambiguous CONNECT request target");
}
let Some(host) = host else {
return Error::e_explain(
InvalidHTTPHeader,
"missing Host header for absolute-form CONNECT request target",
);
};
match http::uri::Authority::try_from(authority) {
Ok(parsed) => reconcile_connect_host(authority, parsed.host().as_bytes(), host),
Err(_) => reconcile_connect_host(authority, authority, host),
}
}
RawTargetAuthority::AmbiguousAuthority => {
Error::e_explain(InvalidHTTPHeader, "ambiguous CONNECT request target")
}
RawTargetAuthority::None if !authority_target.is_empty() && parsed_authority.is_err() => {
let Some(host) = host else {
return Error::e_explain(
InvalidHTTPHeader,
"missing Host header for malformed CONNECT request target",
);
};
reconcile_connect_host(authority_target, authority_target, host)
}
RawTargetAuthority::None => Ok(()),
}
}
fn reconcile_connect_host(
authority: &[u8],
authority_host: &[u8],
host: &HeaderValue,
) -> Result<()> {
if host.as_bytes() == authority || host.as_bytes() == authority_host {
Ok(())
} else {
Error::e_explain(
InvalidHTTPHeader,
"Host header differs from CONNECT request target",
)
}
}
pub(super) fn has_ambiguous_port_suffix(authority: &[u8]) -> bool {
let mut inside_brackets = authority.first() == Some(&b'[');
let mut suffix_colon_seen = false;
for (index, &byte) in authority.iter().enumerate() {
if inside_brackets {
if index == 0 {
continue;
}
match byte {
b'[' => return true,
b']' => inside_brackets = false,
_ => {}
}
continue;
}
match byte {
b'[' | b']' => return true,
b':' if suffix_colon_seen => return true,
b':' => suffix_colon_seen = true,
_ => {}
}
}
inside_brackets
}
#[cfg(test)]
mod tests {
use super::*;
fn request(method: &str, target: &str, hosts: &[&str]) -> RequestHeader {
let mut request = RequestHeader::build(method, target.as_bytes(), None).unwrap();
for host in hosts {
request.append_header(header::HOST, *host).unwrap();
}
request
}
#[test]
fn validate_authority_sources() {
assert!(validate_request_authority(&request("GET", "/test", &[])).is_ok());
assert!(
validate_request_authority(&request("GET", "/test", &["authority.example:8443"]))
.is_ok()
);
assert!(validate_request_authority(&request(
"GET",
"/test",
&["authority.example", "other.example"]
))
.is_err());
assert!(validate_request_authority(&request(
"GET",
"/test",
&["authority.example", "authority.example"]
))
.is_err());
assert!(
validate_request_authority(&request("GET", "/test", &["user@authority.example"]))
.is_err()
);
assert!(validate_request_authority(&request(
"GET",
"/test",
&["user:pass@authority.example"]
))
.is_err());
assert!(validate_request_authority(&request(
"GET",
"/test",
&["user%40authority.example"]
))
.is_ok());
let uri_request = |uri: &str, host: Option<&str>| {
let mut request = http::Request::builder().uri(uri).body(()).unwrap();
if let Some(host) = host {
request
.headers_mut()
.insert(header::HOST, HeaderValue::from_str(host).unwrap());
}
RequestHeader::from(request.into_parts().0)
};
assert!(validate_request_authority(&uri_request(
"https://user@authority.example/test",
None
))
.is_err());
assert!(validate_request_authority(&uri_request(
"https://authority.example/test",
Some("other.example")
))
.is_err());
}
#[test]
fn validate_absolute_form() {
assert!(validate_request_authority(&request(
"GET",
"http://authority.example/test",
&["authority.example"]
))
.is_ok());
assert!(validate_request_authority(&request(
"GET",
"http://user@authority.example/test",
&["authority.example"]
))
.is_err());
for authority in [
"good.example%5Cevil.example",
"user%40good.example",
"a%2f.example",
"good.example:443:8080",
"[good.example",
] {
let target = format!("http://{authority}/test");
let err =
validate_request_authority(&request("GET", &target, &[authority])).unwrap_err();
assert_eq!(
err.context.as_ref().map(|context| context.as_str()),
Some("unparseable absolute-form authority"),
"{target}"
);
}
for target in ["http:///path", "https://?query"] {
let err = validate_request_authority(&request("GET", target, &[""])).unwrap_err();
assert_eq!(
err.context.as_ref().map(|context| context.as_str()),
Some("ambiguous HTTP absolute-form request target"),
"{target}"
);
}
for target in ["ht_tp://other.example/admin", "9x://other.example/admin"] {
assert!(
validate_request_authority(&request("GET", target, &["other.example"])).is_err(),
"{target}"
);
}
let mut hostless = request("GET", "http://authority.example/test", &[]);
assert!(validate_request_authority(&hostless).is_ok());
hostless.set_version(http::Version::HTTP_10);
assert!(validate_request_authority(&hostless).is_ok());
let err = validate_request_authority(&request(
"GET",
"http://authority.example/test",
&["other.example"],
))
.unwrap_err();
assert_eq!(
err.context.as_ref().map(|context| context.as_str()),
Some("Host header differs from request-target authority")
);
let err = validate_request_authority(&request(
"GET",
"http:/\\/\\other.example/admin",
&["authority.example"],
))
.unwrap_err();
assert_eq!(
err.context.as_ref().map(|context| context.as_str()),
Some("ambiguous HTTP absolute-form request target")
);
let err = validate_request_authority(&request(
"GET",
"http://good.example\\evil.example/",
&["good.example\\evil.example"],
))
.unwrap_err();
assert_eq!(
err.context.as_ref().map(|context| context.as_str()),
Some("ambiguous HTTP absolute-form request target")
);
assert!(validate_request_authority(&request("GET", "/users/foo@bar.example", &[])).is_ok());
assert!(
validate_request_authority(&request("GET", "/test?email=foo@bar.example", &[])).is_ok()
);
assert!(validate_request_authority(&request(
"GET",
"/redirect?next=http://other.example/admin",
&["authority.example"]
))
.is_ok());
assert!(validate_request_authority(&request(
"GET",
"/test#http://user@evil.example/",
&["authority.example"]
))
.is_ok());
}
#[test]
fn validate_connect() {
for target in [
"/",
"/foo",
"/ws?arr[]=1",
"/ws?token=a@b.com",
"?query",
"?a=:1:2",
"#frag",
] {
assert!(
validate_request_authority(&request("CONNECT", target, &["other.example"])).is_ok(),
"{target} should remain application-defined"
);
}
assert!(validate_request_authority(&request("CONNECT", "http:443", &["http"])).is_ok());
assert!(
validate_request_authority(&request("CONNECT", "[::1]", &["other.example"])).is_ok()
);
assert!(validate_request_authority(&request("CONNECT", "[::1]:443", &["[::1]"])).is_ok());
assert!(
validate_request_authority(&request("CONNECT", "authority.example:443", &[])).is_ok()
);
assert!(validate_request_authority(&request(
"CONNECT",
"authority.example:443",
&["authority.example:443"]
))
.is_ok());
assert!(validate_request_authority(&request(
"CONNECT",
"authority.example:443/path?arr[]=1&port=:1:2",
&["authority.example:443"]
))
.is_ok());
assert!(validate_request_authority(&request(
"CONNECT",
"unix:/var/run/x.sock",
&["other.example"]
))
.is_ok());
for target in [
"a.example",
"a.example:",
"a.example:443x",
"a.example:65536",
"myproto:opaque",
] {
assert!(
validate_request_authority(&request("CONNECT", target, &["other.example"])).is_ok(),
"{target} should remain application-defined"
);
}
for target in ["a%2f.example:443", "[0:1:2:3:4:5:6:7:8:9]:443"] {
assert!(
validate_request_authority(&request("CONNECT", target, &[target])).is_ok(),
"{target} should be accepted when Host is byte-identical"
);
}
assert!(validate_request_authority(&request(
"CONNECT",
"http://a%2f.example:443/",
&["a%2f.example:443"]
))
.is_ok());
for target in [
"authority.example:443",
"authority.example:443/path",
"authority.example:443?query",
"authority.example:443:80",
"[::1]:443:80",
"a.example:80x:443",
"[a.example:443:80",
"a.example:443[:80]",
"[::1]:443",
"::1",
"a%2f.example:443",
"[a[b]:443",
"[0:1:2:3:4:5:6:7:8:9]:443",
"http://a%2f.example:443/",
"https://authority.example/",
] {
assert!(
validate_request_authority(&request("CONNECT", target, &["other.example"]))
.is_err(),
"{target} should be rejected"
);
}
assert!(validate_request_authority(&request(
"CONNECT",
"a.example:443:80",
&["a.example:443:80"]
))
.is_err());
assert!(
validate_request_authority(&request("CONNECT", "a[b]:443", &["a[b]:443"])).is_err()
);
assert!(
validate_request_authority(&request("CONNECT", "[a[b]:443", &["[a[b]:443"])).is_err()
);
assert!(
validate_request_authority(&request("CONNECT", "http://a[b]:443/", &["a[b]:443"]))
.is_err()
);
assert!(validate_request_authority(&request(
"CONNECT",
"http://user@authority.example:443/",
&["user@authority.example:443"]
))
.is_err());
let mut non_utf8 = RequestHeader::build("CONNECT", b"a.example:443\xff#:80", None).unwrap();
non_utf8.append_header(header::HOST, "a.example").unwrap();
assert!(validate_request_authority(&non_utf8).is_err());
let mut non_utf8 = RequestHeader::build("CONNECT", b"a\xff.example:443", None).unwrap();
non_utf8
.append_header(header::HOST, "other.example")
.unwrap();
assert!(validate_request_authority(&non_utf8).is_err());
}
}