Skip to content

Commit 664cd1a

Browse files
authored
fix: prevent Host header bypass vulnerability (#58)
This commit fixes a security vulnerability where an attacker could bypass proxy restrictions by sending requests with mismatched Host headers. The vulnerability allowed sending a request to one domain (e.g., api.anthropic.com) while setting the Host header to another domain (e.g., evil.com), which could cause CDNs like CloudFlare to route the request to the attacker's server. Thank you to @amyb-asu for reporting this in #57
1 parent 0c1683c commit 664cd1a

File tree

2 files changed

+78
-1
lines changed

2 files changed

+78
-1
lines changed

src/proxy.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,37 @@ pub fn prepare_upstream_request(
7272
let (mut parts, incoming_body) = req.into_parts();
7373

7474
// Update the URI
75-
parts.uri = target_uri;
75+
parts.uri = target_uri.clone();
7676

7777
// Remove proxy-specific headers only
7878
// Don't remove connection-related headers as the client will handle them
7979
parts.headers.remove("proxy-connection");
8080
parts.headers.remove("proxy-authorization");
8181
parts.headers.remove("proxy-authenticate");
8282

83+
// SECURITY: Ensure the Host header matches the URI to prevent routing bypasses (Issue #57)
84+
// This prevents attacks where an attacker sends a request to one domain but sets
85+
// the Host header to another domain, potentially bypassing security controls in
86+
// CDNs like CloudFlare that route based on the Host header.
87+
if let Some(authority) = target_uri.authority() {
88+
debug!(
89+
"Setting Host header to match URI authority: {}",
90+
authority.as_str()
91+
);
92+
parts.headers.insert(
93+
hyper::header::HOST,
94+
hyper::header::HeaderValue::from_str(authority.as_str())
95+
.unwrap_or_else(|_| hyper::header::HeaderValue::from_static("unknown")),
96+
);
97+
}
98+
99+
// TODO: Future improvement - Use the type system to ensure security guarantees
100+
// We should eventually refactor this to use types that guarantee all request
101+
// information passed to upstream has been validated by the RuleEngine.
102+
// For example, we could have a `ValidatedRequest` type that can only be
103+
// constructed after passing through rule evaluation, making it impossible
104+
// to accidentally forward unvalidated or modified headers to the upstream.
105+
83106
// Convert incoming body to boxed body
84107
let boxed_request_body = incoming_body.boxed();
85108

tests/weak_integration.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,3 +287,57 @@ fn test_server_mode() {
287287
let _ = server.kill();
288288
let _ = server.wait();
289289
}
290+
291+
/// Test for Host header security (Issue #57)
292+
/// Verifies that httpjail corrects mismatched Host headers to prevent
293+
/// CloudFlare and other CDN routing bypasses.
294+
#[test]
295+
fn test_host_header_security() {
296+
use std::process::Command;
297+
298+
// Define the same curl command that attempts to set a mismatched Host header
299+
let curl_args = vec![
300+
"-s",
301+
"-H",
302+
"Host: evil.com",
303+
"--max-time",
304+
"3",
305+
"http://httpbin.org/headers",
306+
];
307+
308+
// Test 1: Direct curl execution (without httpjail) - shows the vulnerability
309+
let direct_result = Command::new("curl")
310+
.args(&curl_args)
311+
.output()
312+
.expect("Failed to execute curl directly");
313+
314+
let direct_stdout = String::from_utf8_lossy(&direct_result.stdout);
315+
assert!(
316+
direct_stdout.contains("\"Host\": \"evil.com\""),
317+
"Direct curl should pass through the evil.com Host header unchanged"
318+
);
319+
320+
// Test 2: Same curl command through httpjail - shows the fix
321+
let httpjail_result = HttpjailCommand::new()
322+
.weak()
323+
.js("true") // Allow all requests
324+
.command(vec!["curl"].into_iter().chain(curl_args).collect())
325+
.execute();
326+
327+
assert!(httpjail_result.is_ok(), "Httpjail request should complete");
328+
let (exit_code, stdout, _) = httpjail_result.unwrap();
329+
assert_eq!(exit_code, 0, "Httpjail request should succeed");
330+
331+
// Httpjail should have corrected the Host header to match the URI
332+
assert!(
333+
stdout.contains("\"Host\": \"httpbin.org\""),
334+
"Httpjail should correct the Host header to httpbin.org"
335+
);
336+
assert!(
337+
!stdout.contains("\"Host\": \"evil.com\""),
338+
"Httpjail should not pass through the evil.com Host header"
339+
);
340+
341+
// This demonstrates that httpjail prevents the Host header bypass attack
342+
// that would otherwise be possible with direct curl execution
343+
}

0 commit comments

Comments
 (0)