Compare commits

...

2 Commits

Author SHA1 Message Date
allcontributors[bot]
378d75964c docs: add aldamd as a contributor for code (#1309)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2026-04-15 08:31:37 -04:00
Daniel Aldam
ffdf871abe fix: support LF-only line endings in --request-file parsing (#1306)
* fix: support LF-only line endings in --request-file parsing
* preserve raw body when parsing request file
* Added CRLFCRLF/LFLF request-file parsing tests
* better invalid UTF-8 in request file header error message
* strengthened request-file mixed CRLF LF headers test
* added request-file explicit binary body test
* cargo fmt
* updated request-file header-body separation logic to choose first occurrence & added testing
2026-04-15 08:30:39 -04:00
3 changed files with 188 additions and 15 deletions

View File

@@ -1038,6 +1038,15 @@
"contributions": [
"bug"
]
},
{
"login": "aldamd",
"name": "Daniel Aldam",
"avatar_url": "https://avatars.githubusercontent.com/u/178115486?v=4",
"profile": "https://github.com/aldamd",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@@ -373,6 +373,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Antonio-R1"><img src="https://avatars.githubusercontent.com/u/54741970?v=4?s=100" width="100px;" alt="Antonio"/><br /><sub><b>Antonio</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=Antonio-R1" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/issues?q=author%3AAntonio-R1" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/redacean"><img src="https://avatars.githubusercontent.com/u/125687454?v=4?s=100" width="100px;" alt="Redacean"/><br /><sub><b>Redacean</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Aredacean" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ghsdpolley"><img src="https://avatars.githubusercontent.com/u/19826831?v=4?s=100" width="100px;" alt="ghsdpolley"/><br /><sub><b>ghsdpolley</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Aghsdpolley" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aldamd"><img src="https://avatars.githubusercontent.com/u/178115486?v=4?s=100" width="100px;" alt="Daniel Aldam"/><br /><sub><b>Daniel Aldam</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=aldamd" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -381,32 +381,54 @@ impl ContentType {
/// unless overridden by CLI options.
///
pub fn parse_request_file(config: &mut Configuration) -> Result<()> {
// read in the file located at config.request_file
// read in the file (raw bytes) located at config.request_file
// parse the file into a Request struct
let contents = std::fs::read_to_string(&config.request_file)?;
let contents = std::fs::read(&config.request_file)?;
if contents.is_empty() {
bail!("Empty --request-file file provided");
}
// this should split the body from the request line and headers
let lines = contents.split("\r\n\r\n").collect::<Vec<&str>>();
// find the first header/body separator
// locate both \r\n\r\n and \n\n and pick whichever appears earliest,
// so that a \r\n\r\n inside the body doesn't shadow a \n\n separator
// that terminates the headers
let crlf = contents.windows(4).position(|w| w == b"\r\n\r\n");
let lf = contents.windows(2).position(|w| w == b"\n\n");
if lines.len() < 2 {
bail!("Invalid request: Missing head/body CRLF separator");
}
let (sep_idx, sep_len) = match (crlf, lf) {
(Some(c), Some(l)) => {
if c <= l {
(c, 4)
} else {
(l, 2)
}
}
(Some(c), None) => (c, 4),
(None, Some(l)) => (l, 2),
(None, None) => bail!("Invalid request: Missing head/body separator"),
};
let head = lines[0];
let body = lines[1].as_bytes().to_vec();
// split the request head and body
let head_bytes = &contents[..sep_idx];
let body_bytes = &contents[sep_idx + sep_len..];
// we only want to use the request's body if the user hasn't
// decode only the head; HTTP framing is generally ascii/utf-8
// compatible
let head = std::str::from_utf8(head_bytes)
.map_err(|_| anyhow::anyhow!("Request headers contain invalid UTF-8"))?;
// normalize line endings in the decoded head
let normalized = head.replace("\r\n", "\n");
// we only want to use the request's body bytes if the user hasn't
// overridden it on the cli
if config.data.is_empty() {
config.data = body;
config.data = body_bytes.to_vec();
}
// begin parsing the request line and headers
let mut head_parts = head.split("\r\n");
// begin parsing the request line and normalized headers
let mut head_parts = normalized.split("\n");
let Some(request_line) = head_parts.next() else {
bail!("Invalid request: Missing request line");
@@ -441,7 +463,7 @@ pub fn parse_request_file(config: &mut Configuration) -> Result<()> {
}
for mut line in head_parts {
line = line.trim();
line = line.trim_matches('\r').trim();
if line.is_empty() {
break; // Empty line signals the end of headers
@@ -904,7 +926,7 @@ mod tests {
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Invalid request: Missing head/body CRLF separator"
"Invalid request: Missing head/body separator"
);
tmp.cleanup();
@@ -1345,4 +1367,145 @@ mod tests {
let result = split_query("");
assert!(result.is_err());
}
#[test]
fn test_parse_raw_lf_only_request() -> io::Result<()> {
let mut tmp = TempSetup::new();
tmp.file
.write_all(b"GET / HTTP/1.1\nHost: example.com\n\nbody")?;
let result = parse_request_file(&mut tmp.config);
assert!(result.is_ok());
assert_eq!(tmp.config.data, b"body".to_vec());
tmp.cleanup();
Ok(())
}
#[test]
fn test_parse_raw_crlf_request() -> io::Result<()> {
let mut tmp = TempSetup::new();
tmp.file
.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\nbody")?;
let result = parse_request_file(&mut tmp.config);
assert!(result.is_ok());
assert_eq!(tmp.config.data, b"body".to_vec());
tmp.cleanup();
Ok(())
}
#[test]
fn test_parse_raw_crlf_body_preserved() -> io::Result<()> {
let mut tmp = TempSetup::new();
let body = b"line1\r\nline2\r\nbinary\x00data";
let mut request = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".to_vec();
request.extend_from_slice(body);
tmp.file.write_all(&request)?;
parse_request_file(&mut tmp.config).unwrap();
assert_eq!(tmp.config.data, body.to_vec());
tmp.cleanup();
Ok(())
}
#[test]
fn test_parse_raw_lf_headers_crlf_body() -> io::Result<()> {
let mut tmp = TempSetup::new();
let body = b"line1\r\nline2\r\n";
let mut request = b"GET / HTTP/1.1\nHost: example.com\n\n".to_vec();
request.extend_from_slice(body);
tmp.file.write_all(&request)?;
parse_request_file(&mut tmp.config).unwrap();
assert_eq!(tmp.config.data, body.to_vec());
tmp.cleanup();
Ok(())
}
#[test]
fn test_parse_raw_mixed_newlines_headers() -> io::Result<()> {
let mut tmp = TempSetup::new();
tmp.file
.write_all(b"GET / HTTP/1.1\r\nHost: example.com\nUser-Agent: test\r\n\nbody")?;
let result = parse_request_file(&mut tmp.config);
assert!(result.is_ok());
assert_eq!(tmp.config.data, b"body".to_vec());
assert!(tmp.config.headers.contains_key("Host"));
assert_eq!(tmp.config.headers.get("Host").unwrap(), "example.com");
assert_eq!(tmp.config.user_agent, "test");
tmp.cleanup();
Ok(())
}
#[test]
fn test_parse_raw_binary_body_preserved() -> io::Result<()> {
let mut tmp = TempSetup::new();
let body = b"\x00\xde\xad\xbe\xef\x80binary";
let mut request = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".to_vec();
request.extend_from_slice(body);
tmp.file.write_all(&request)?;
parse_request_file(&mut tmp.config).unwrap();
assert_eq!(tmp.config.data, body.to_vec());
tmp.cleanup();
Ok(())
}
#[test]
fn test_parse_raw_lf_headers_with_crlf_crlf_in_body() -> io::Result<()> {
// headers are LF-separated; body contains \r\n\r\n
let mut tmp = TempSetup::new();
tmp.file.write_all(
b"POST /upload HTTP/1.1\nHost: example.com\nContent-Type: application/octet-stream\n\nabc\r\n\r\ndef",
)?;
let result = parse_request_file(&mut tmp.config);
assert!(result.is_ok());
assert_eq!(tmp.config.data, b"abc\r\n\r\ndef".to_vec());
assert_eq!(tmp.config.target_url, "https://example.com/upload");
tmp.cleanup();
Ok(())
}
#[test]
fn test_parse_raw_crlf_headers_with_lf_lf_in_body() -> io::Result<()> {
let mut tmp = TempSetup::new();
tmp.file
.write_all(b"POST /upload HTTP/1.1\r\nHost: example.com\r\n\r\nabc\n\ndef")?;
parse_request_file(&mut tmp.config).unwrap();
assert_eq!(tmp.config.data, b"abc\n\ndef".to_vec());
tmp.cleanup();
Ok(())
}
}