Address feedback

This commit is contained in:
boatbomber 2026-06-03 12:32:06 -07:00
parent 5ab1ab5d5a
commit a90381cb13
5 changed files with 262 additions and 68 deletions

View file

@ -247,9 +247,13 @@ impl TestServeSession {
}
/// Sends a GET to `/api/rojo` with the given extra request headers and
/// returns the response status code. Used to exercise the Host/Origin
/// allowlist that guards against DNS rebinding.
pub fn api_rojo_status_with_headers(&self, headers: &[(&str, &str)]) -> reqwest::StatusCode {
/// returns the full response. Used to exercise the Host/Origin allowlist that
/// guards against DNS rebinding, including asserting that a rejection reveals
/// nothing about the server.
pub fn api_rojo_response_with_headers(
&self,
headers: &[(&str, &str)],
) -> reqwest::blocking::Response {
let client = reqwest::blocking::Client::new();
let url = format!("http://localhost:{}/api/rojo", self.port);
@ -258,7 +262,7 @@ impl TestServeSession {
request = request.header(*name, *value);
}
request.send().expect("Failed to send request").status()
request.send().expect("Failed to send request")
}
/// Sends a POST to `/api/open/<id>` and returns the response status code.

View file

@ -18,29 +18,52 @@ fn rejects_dns_rebinding_requests() {
// A request carrying a local Host header is served normally.
assert_eq!(
session.api_rojo_status_with_headers(&[("host", &local_host)]),
session
.api_rojo_response_with_headers(&[("host", &local_host)])
.status(),
reqwest::StatusCode::OK,
);
// A request whose Host is a foreign hostname, as a DNS-rebound page
// would send, is rejected.
assert_eq!(
session.api_rojo_status_with_headers(&[("host", "evil.com")]),
reqwest::StatusCode::FORBIDDEN,
);
// would send, is rejected with a generic 404 that reveals nothing about
// the server.
assert_rejected(session.api_rojo_response_with_headers(&[("host", "evil.com")]));
// Even with a local Host, a present-but-foreign Origin is rejected.
let foreign_origin = format!("http://evil.com:{port}");
assert_eq!(
session.api_rojo_status_with_headers(&[
assert_rejected(
session.api_rojo_response_with_headers(&[
("host", &local_host),
("origin", &foreign_origin),
]),
reqwest::StatusCode::FORBIDDEN,
);
});
}
/// Asserts that a Host/Origin rejection is a generic 404 whose body and
/// content-type do not identify the server as Rojo.
fn assert_rejected(response: reqwest::blocking::Response) {
assert_eq!(response.status(), reqwest::StatusCode::NOT_FOUND);
let content_type = response
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_owned();
assert!(
!content_type.contains("msgpack"),
"rejection should not use the msgpack API content-type, got {content_type:?}",
);
let body = response.text().expect("Failed to read response body");
let body_lower = body.to_lowercase();
assert!(
!body_lower.contains("rojo") && !body_lower.contains("rebinding"),
"rejection body should not identify the server, got {body:?}",
);
}
#[test]
fn allows_api_open_from_loopback_peer() {
run_serve_test("empty", |session, _redactions| {