use fastapi_http::{
HeadersParser, ParseError, ParseLimits, ParseStatus, Parser, RequestLine, StatefulParser,
};
use std::fmt::Write;
#[test]
fn smuggling_cl_te_basic() {
let buffer = b"POST /admin HTTP/1.1\r\n\
Content-Length: 13\r\n\
Transfer-Encoding: chunked\r\n\r\n\
0\r\n\r\nSMUGGLED";
let parser = Parser::new();
let result = parser.parse(buffer);
assert!(
matches!(result, Err(ParseError::AmbiguousBodyLength)),
"CL.TE smuggling attempt should be rejected"
);
}
#[test]
fn smuggling_te_cl_basic() {
let buffer = b"POST /admin HTTP/1.1\r\n\
Transfer-Encoding: chunked\r\n\
Content-Length: 4\r\n\r\n\
5c\r\nGPOST / HTTP/1.1\r\n\r\n0\r\n\r\n";
let parser = Parser::new();
let result = parser.parse(buffer);
assert!(
matches!(result, Err(ParseError::AmbiguousBodyLength)),
"TE.CL smuggling attempt should be rejected"
);
}
#[test]
fn smuggling_cl_cl_different_values() {
let buffer = b"Content-Length: 10\r\nContent-Length: 20\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(
matches!(result, Err(ParseError::InvalidHeader)),
"Different Content-Length values should be rejected"
);
}
#[test]
fn smuggling_cl_cl_same_value_ok() {
let buffer = b"Content-Length: 42\r\nContent-Length: 42\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(result.is_ok());
assert_eq!(result.unwrap().content_length(), Some(42));
}
#[test]
fn smuggling_http09_downgrade() {
let buffer = b"GET /\r\n";
let result = RequestLine::parse(buffer);
assert!(result.is_err());
}
#[test]
fn smuggling_chunk_extension() {
let buffer = b"Host: example.com\r\nTransfer-Encoding: chunked\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
assert!(parser.is_chunked());
}
#[test]
fn smuggling_te_unexpected_value() {
let buffer = b"Transfer-Encoding: gzip\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidTransferEncoding)));
}
#[test]
fn smuggling_te_chunked_trailing() {
let buffer = b"Transfer-Encoding: chunked, identity\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(result.is_ok() || result.is_err());
}
#[test]
fn injection_crlf_in_path() {
let buffer = b"GET /path\r\nX-Injected: evil HTTP/1.1\r\n\r\n";
let result = RequestLine::parse(buffer);
if let Ok(line) = result {
assert!(
!line.path().contains('\r') && !line.path().contains('\n'),
"Path should not contain CRLF characters"
);
}
}
#[test]
fn injection_crlf_url_encoded() {
let buffer = b"GET /path%0d%0aX-Injected:%20evil HTTP/1.1\r\n\r\n";
let parser = Parser::new();
let result = parser.parse(buffer);
if let Ok(request) = result {
assert!(request.headers().get("X-Injected").is_none());
} else {
}
}
#[test]
fn injection_double_crlf() {
let buffer = b"GET /path\r\n\r\nHTTP/1.1\r\n\r\n";
let result = RequestLine::parse(buffer);
assert!(result.is_err() || result.is_ok());
}
#[test]
fn injection_header_value_crlf() {
let buffer = b"X-Test: value\r\nX-Injected: evil\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
assert!(parser.get("X-Test").is_some());
assert!(parser.get("X-Injected").is_some());
}
#[test]
fn injection_null_byte_request_line() {
let buffer = b"GET /path\x00evil HTTP/1.1\r\n";
let result = RequestLine::parse(buffer);
assert!(
matches!(result, Err(ParseError::InvalidRequestLine)),
"Null bytes in request line should be rejected"
);
}
#[test]
fn injection_null_byte_header_name() {
let buffer = b"X-Test\x00Header: value\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(
result.is_err(),
"Null bytes in header name should be rejected"
);
}
#[test]
fn injection_null_byte_header_value() {
let buffer = b"X-Test: val\x00ue\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(
matches!(result, Err(ParseError::InvalidHeaderBytes)),
"Null bytes in header value should be rejected"
);
}
#[test]
fn injection_obs_fold() {
let buffer = b"X-Test: value\r\n continuation\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(
matches!(result, Err(ParseError::InvalidHeader)),
"Obsolete line folding should be rejected"
);
}
#[test]
fn traversal_dot_dot_slash() {
let buffer = b"GET /../../../etc/passwd HTTP/1.1\r\n\r\n";
let parser = Parser::new();
let result = parser.parse(buffer);
if let Ok(request) = result {
assert!(request.path().contains("..") || request.path().contains("etc"));
} else {
}
}
#[test]
fn traversal_url_encoded() {
let buffer = b"GET /%2e%2e/%2e%2e/etc/passwd HTTP/1.1\r\n\r\n";
let parser = Parser::new();
let result = parser.parse(buffer);
if let Ok(request) = result {
let path = request.path();
assert!(!path.is_empty());
} else {
}
}
#[test]
fn traversal_double_encoded() {
let buffer = b"GET /%252e%252e/etc/passwd HTTP/1.1\r\n\r\n";
let parser = Parser::new();
let result = parser.parse(buffer);
if let Ok(request) = result {
assert!(!request.path().is_empty());
} else {
}
}
#[test]
fn traversal_backslash() {
let buffer = b"GET /..\\..\\etc\\passwd HTTP/1.1\r\n\r\n";
let parser = Parser::new();
let result = parser.parse(buffer);
if let Ok(request) = result {
assert!(request.path().contains("..\\") || request.path().contains("etc"));
} else {
}
}
#[test]
fn traversal_null_byte_truncation() {
let buffer = b"GET /admin\x00.jpg HTTP/1.1\r\n";
let result = RequestLine::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidRequestLine)));
}
#[test]
fn traversal_overlong_utf8() {
let buffer = b"GET /\xc0\xae\xc0\xae/etc/passwd HTTP/1.1\r\n\r\n";
let parser = Parser::new();
let _ = parser.parse(buffer);
}
#[test]
fn exhaustion_long_request_line() {
let limits = ParseLimits {
max_request_line_len: 100,
..Default::default()
};
let long_path = "a".repeat(200);
let buffer = format!("GET /{long_path} HTTP/1.1\r\n\r\n");
let mut parser = StatefulParser::new().with_limits(limits);
let result = parser.feed(buffer.as_bytes());
assert!(
matches!(result, Err(ParseError::RequestLineTooLong)),
"Extremely long request line should be rejected"
);
}
#[test]
fn exhaustion_too_many_headers() {
let limits = ParseLimits {
max_header_count: 5,
..Default::default()
};
let mut buffer = String::new();
for i in 0..10 {
let _ = write!(buffer, "X-Header-{i}: value\r\n");
}
buffer.push_str("\r\n");
let result = HeadersParser::parse_with_limits(buffer.as_bytes(), &limits);
assert!(
matches!(result, Err(ParseError::TooManyHeaders)),
"Too many headers should be rejected"
);
}
#[test]
fn exhaustion_long_header_line() {
let limits = ParseLimits {
max_header_line_len: 100,
..Default::default()
};
let long_value = "x".repeat(200);
let buffer = format!("X-Long: {long_value}\r\n\r\n");
let result = HeadersParser::parse_with_limits(buffer.as_bytes(), &limits);
assert!(
matches!(result, Err(ParseError::HeaderLineTooLong)),
"Extremely long header line should be rejected"
);
}
#[test]
fn exhaustion_large_headers_block() {
let limits = ParseLimits {
max_headers_size: 100,
..Default::default()
};
let mut buffer = String::new();
for i in 0..20 {
let _ = write!(buffer, "X-H{i}: value{i}\r\n");
}
buffer.push_str("\r\n");
let result = HeadersParser::parse_with_limits(buffer.as_bytes(), &limits);
assert!(
matches!(result, Err(ParseError::HeadersTooLarge)),
"Extremely large headers block should be rejected"
);
}
#[test]
fn exhaustion_huge_content_length() {
let buffer = b"Content-Length: 99999999999999999999999999\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(
matches!(result, Err(ParseError::InvalidHeader)),
"Huge Content-Length should be rejected"
);
}
#[test]
fn exhaustion_negative_content_length() {
let buffer = b"Content-Length: -1\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(
matches!(result, Err(ParseError::InvalidHeader)),
"Negative Content-Length should be rejected"
);
}
#[test]
fn exhaustion_non_numeric_content_length() {
let buffer = b"Content-Length: 10abc\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(
matches!(result, Err(ParseError::InvalidHeader)),
"Non-numeric Content-Length should be rejected"
);
}
#[test]
fn exhaustion_chunk_size_overflow() {
let mut parser = StatefulParser::new();
let buffer = b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\nFFFFFFFFFFFFFFFFF\r\n";
let result = parser.feed(buffer);
assert!(
matches!(result, Err(ParseError::InvalidHeader)),
"chunk size overflow must be rejected"
);
}
#[test]
fn exhaustion_request_too_large() {
let mut parser = StatefulParser::new().with_max_size(100);
let buffer = b"GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 200\r\n\r\n";
let mut result = parser.feed(buffer);
if matches!(result, Ok(ParseStatus::Incomplete)) {
let body = vec![b'x'; 200];
result = parser.feed(&body);
}
assert!(matches!(result, Err(ParseError::TooLarge)));
}
#[test]
fn encoding_mixed_case_method() {
let buffer = b"Get /path HTTP/1.1\r\n";
let result = RequestLine::parse(buffer);
assert!(
matches!(result, Err(ParseError::InvalidMethod)),
"Mixed-case method should be rejected"
);
}
#[test]
fn encoding_lowercase_method() {
let buffer = b"get /path HTTP/1.1\r\n";
let result = RequestLine::parse(buffer);
assert!(
matches!(result, Err(ParseError::InvalidMethod)),
"Lowercase method should be rejected"
);
}
#[test]
fn encoding_tab_separator() {
let buffer = b"GET\t/path\tHTTP/1.1\r\n";
let result = RequestLine::parse(buffer);
assert!(result.is_err());
}
#[test]
fn encoding_lf_only() {
let buffer = b"GET /path HTTP/1.1\nHost: example.com\n\n";
let parser = Parser::new();
let result = parser.parse(buffer);
assert!(result.is_err());
}
#[test]
fn encoding_cr_only() {
let buffer = b"GET /path HTTP/1.1\rHost: example.com\r\r";
let parser = Parser::new();
let result = parser.parse(buffer);
assert!(result.is_err());
}
#[test]
fn encoding_invalid_utf8_path() {
let buffer = b"GET /\xff\xfe HTTP/1.1\r\n\r\n";
let parser = Parser::new();
let result = parser.parse(buffer);
let _ = result;
}
#[test]
fn encoding_invalid_utf8_header() {
let buffer = b"X-Binary: \xff\xfe\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
let header = parser.get("X-Binary").unwrap();
assert!(header.value_str().is_none());
assert_eq!(header.value(), b"\xff\xfe");
}
#[test]
fn encoding_percent_special_chars() {
let buffer = b"GET /%00%0d%0a HTTP/1.1\r\n\r\n";
let parser = Parser::new();
let result = parser.parse(buffer);
let _ = result;
}
#[test]
fn encoding_unicode_normalization() {
let buffer = "GET /caf\u{00e9} HTTP/1.1\r\n\r\n".as_bytes();
let parser = Parser::new();
let result1 = parser.parse(buffer);
let buffer2 = "GET /cafe\u{0301} HTTP/1.1\r\n\r\n".as_bytes();
let result2 = parser.parse(buffer2);
let _ = result1;
let _ = result2;
}
#[test]
fn cve_apache_chunked_pattern() {
let buffer = b"Transfer-Encoding: chunked\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
assert!(parser.is_chunked());
}
#[test]
fn cve_nginx_buffer_pattern() {
let long_path = "A".repeat(10000);
let buffer = format!("GET /{long_path} HTTP/1.1\r\n\r\n");
let parser = Parser::new();
let _ = parser.parse(buffer.as_bytes());
}
#[test]
fn cve_response_splitting() {
let buffer = b"X-Header: value\r\nContent-Type: text/html\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
assert!(parser.get("X-Header").is_some());
assert!(parser.get("Content-Type").is_some());
}
#[test]
fn cve_host_header_injection() {
let buffer = b"Host: evil.com\r\nX-Injected-Host: attack\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
assert_eq!(parser.get("Host").unwrap().value_str(), Some("evil.com"));
assert!(parser.get("X-Injected-Host").is_some());
}
#[test]
fn combined_smuggling_traversal() {
let buffer = b"POST /../admin HTTP/1.1\r\n\
Content-Length: 0\r\n\
Transfer-Encoding: chunked\r\n\r\n";
let parser = Parser::new();
let result = parser.parse(buffer);
assert!(matches!(result, Err(ParseError::AmbiguousBodyLength)));
}
#[test]
fn combined_injection_exhaustion() {
let limits = ParseLimits {
max_header_count: 10,
..Default::default()
};
let mut buffer = String::new();
for i in 0..15 {
let _ = write!(buffer, "X-H{i}: \r\nX-Injected{i}: evil\r\n");
}
buffer.push_str("\r\n");
let result = HeadersParser::parse_with_limits(buffer.as_bytes(), &limits);
assert!(matches!(result, Err(ParseError::TooManyHeaders)));
}
#[test]
fn stateful_handles_attacks() {
let mut parser = StatefulParser::new();
let smuggle = b"POST /x HTTP/1.1\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\n";
let result = parser.feed(smuggle);
assert!(
matches!(result, Err(ParseError::AmbiguousBodyLength)),
"Stateful parser should reject smuggling attempts"
);
}
#[test]
fn smuggling_unknown_http_version_rejected() {
let buffer = b"GET / HTTP/9.9\r\nHost: example.com\r\n\r\n";
let parser = Parser::new();
let result = parser.parse(buffer);
assert!(
result.is_err(),
"Unknown HTTP version should be rejected, got: {result:?}"
);
}
#[test]
fn smuggling_request_line_extra_parts_rejected() {
let buffer = b"GET / HTTP/1.1 extra-data\r\nHost: example.com\r\n\r\n";
let parser = Parser::new();
let result = parser.parse(buffer);
assert!(
result.is_err(),
"Extra request-line parts should be rejected, got: {result:?}"
);
}
#[test]
fn traversal_overlong_utf8_percent_decoded() {
let buffer = b"GET /%C0%AF HTTP/1.1\r\nHost: example.com\r\n\r\n";
let parser = Parser::new();
let result = parser.parse(buffer);
assert!(
result.is_err(),
"Overlong UTF-8 in percent-decoded path should be rejected, got: {result:?}"
);
}
#[test]
fn smuggling_compound_transfer_encoding_rejected() {
let buffer =
b"POST /x HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: gzip, chunked\r\n\r\n0\r\n\r\n";
let parser = Parser::new();
let result = parser.parse(buffer);
assert!(
result.is_err(),
"Compound TE should be rejected, got: {result:?}"
);
}