mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 17:21:49 -10:00
horizon-server: bound HTTP request and header lines
read_line had no limit, and the read timeout applies per read, so a client streaming bytes without a newline to the API or the web panel grew the supervisor's memory without bound. Lines longer than 8 KiB now get a 400. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
effb29e571
commit
d3be8dbf05
1 file changed
+46
-2
@@ -10,6 +10,8 @@ use std::{
|
||||
|
||||
/// The largest request body the API takes (a console command, a settings change).
|
||||
const MAX_BODY: usize = 1 << 20;
|
||||
/// The longest request line or header line taken.
|
||||
const MAX_LINE: usize = 8 << 10;
|
||||
|
||||
pub struct Request {
|
||||
pub method: String,
|
||||
@@ -91,11 +93,33 @@ fn reason(status: u16) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// `read_line` under a limit: more than MAX_LINE without a '\n' is an error
|
||||
/// (InvalidInput), not a line that grows for as long as the client streams.
|
||||
fn read_line_bounded(reader: &mut impl BufRead, line: &mut String) -> std::io::Result<usize> {
|
||||
let n = reader.by_ref().take(MAX_LINE as u64 + 1).read_line(line)?;
|
||||
if n > MAX_LINE {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"line too long",
|
||||
));
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn read_request(stream: &TcpStream) -> Result<Request, Response> {
|
||||
let bad = |m: &str| Response::error(400, m);
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut line = String::new();
|
||||
reader.read_line(&mut line).map_err(|_| bad("no request"))?;
|
||||
let read_line = |reader: &mut BufReader<&TcpStream>, line: &mut String, what: &str| {
|
||||
read_line_bounded(reader, line).map_err(|e| {
|
||||
if e.kind() == std::io::ErrorKind::InvalidInput {
|
||||
bad("line too long")
|
||||
} else {
|
||||
bad(what)
|
||||
}
|
||||
})
|
||||
};
|
||||
read_line(&mut reader, &mut line, "no request")?;
|
||||
let mut parts = line.split_whitespace();
|
||||
let (Some(method), Some(target)) = (parts.next(), parts.next()) else {
|
||||
return Err(bad("malformed request line"));
|
||||
@@ -104,7 +128,7 @@ fn read_request(stream: &TcpStream) -> Result<Request, Response> {
|
||||
let mut headers = Vec::new();
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
if reader.read_line(&mut line).map_err(|_| bad("headers"))? == 0 || line.trim().is_empty() {
|
||||
if read_line(&mut reader, &mut line, "headers")? == 0 || line.trim().is_empty() {
|
||||
break;
|
||||
}
|
||||
if headers.len() > 64 {
|
||||
@@ -246,4 +270,24 @@ mod tests {
|
||||
assert_eq!(v["auth"], "Bearer t0k");
|
||||
assert_eq!(v["body"], r#"{"line":"list"}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_line_past_the_limit_is_a_400() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
serve(listener, |_| Response::text(200, "text/plain", "ok")).unwrap();
|
||||
let mut stream = TcpStream::connect(address).unwrap();
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap();
|
||||
// A request line that never ends, sent at once so the refused reply
|
||||
// (and the close) can't reset the write side first.
|
||||
let mut flood = b"GET /".to_vec();
|
||||
flood.extend_from_slice(&[b'a'; 100_000]);
|
||||
let _ = stream.write_all(&flood);
|
||||
let _ = stream.shutdown(std::net::Shutdown::Write);
|
||||
let mut text = String::new();
|
||||
let _ = stream.read_to_string(&mut text);
|
||||
assert!(text.starts_with("HTTP/1.1 400"), "{text}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user