Add size limits on responses (#351)
This prevents a malicious server from running the client out of memory by returning a very large body. Limit into_string() to 1 megabyte. Limit individual header fields to 100 kilobytes. Limit number of header fields to 100. Part of #267
This commit is contained in:
committed by
GitHub
parent
40e156e2a3
commit
3044ae7efd
125
src/response.rs
125
src/response.rs
@@ -20,6 +20,11 @@ use encoding_rs::Encoding;
|
|||||||
|
|
||||||
pub const DEFAULT_CONTENT_TYPE: &str = "text/plain";
|
pub const DEFAULT_CONTENT_TYPE: &str = "text/plain";
|
||||||
pub const DEFAULT_CHARACTER_SET: &str = "utf-8";
|
pub const DEFAULT_CHARACTER_SET: &str = "utf-8";
|
||||||
|
const INTO_STRING_LIMIT: usize = 10 * 1_024 * 1_024;
|
||||||
|
// Follow the example of curl and limit a single header to 100kB:
|
||||||
|
// https://curl.se/libcurl/c/CURLOPT_HEADERFUNCTION.html
|
||||||
|
const MAX_HEADER_SIZE: usize = 100 * 1_024;
|
||||||
|
const MAX_HEADER_COUNT: usize = 100;
|
||||||
|
|
||||||
/// Response instances are created as results of firing off requests.
|
/// Response instances are created as results of firing off requests.
|
||||||
///
|
///
|
||||||
@@ -211,6 +216,11 @@ impl Response {
|
|||||||
/// length regardless of how many bytes the server sends.
|
/// length regardless of how many bytes the server sends.
|
||||||
/// 3. If no length header, the reader is until server stream end.
|
/// 3. If no length header, the reader is until server stream end.
|
||||||
///
|
///
|
||||||
|
/// Note: If you use `read_to_end()` on the resulting reader, a malicious
|
||||||
|
/// server might return enough bytes to exhaust available memroy. If you're
|
||||||
|
/// making requests to untrusted servers, you should use `.take()` to
|
||||||
|
/// limit the response bytes read.
|
||||||
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
@@ -226,6 +236,7 @@ impl Response {
|
|||||||
///
|
///
|
||||||
/// let mut bytes: Vec<u8> = Vec::with_capacity(len);
|
/// let mut bytes: Vec<u8> = Vec::with_capacity(len);
|
||||||
/// resp.into_reader()
|
/// resp.into_reader()
|
||||||
|
/// .take(10_000_000)
|
||||||
/// .read_to_end(&mut bytes)?;
|
/// .read_to_end(&mut bytes)?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(bytes.len(), len);
|
/// assert_eq!(bytes.len(), len);
|
||||||
@@ -291,6 +302,8 @@ impl Response {
|
|||||||
/// implementation first reads the reader to end into a `Vec<u8>` and then
|
/// implementation first reads the reader to end into a `Vec<u8>` and then
|
||||||
/// attempts to decode it using the charset.
|
/// attempts to decode it using the charset.
|
||||||
///
|
///
|
||||||
|
/// If the response is larger than 10 megabytes, this will return an error.
|
||||||
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
@@ -321,14 +334,30 @@ impl Response {
|
|||||||
.or_else(|| Encoding::for_label(DEFAULT_CHARACTER_SET.as_bytes()))
|
.or_else(|| Encoding::for_label(DEFAULT_CHARACTER_SET.as_bytes()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut buf: Vec<u8> = vec![];
|
let mut buf: Vec<u8> = vec![];
|
||||||
self.into_reader().read_to_end(&mut buf)?;
|
self.into_reader()
|
||||||
|
.take((INTO_STRING_LIMIT + 1) as u64)
|
||||||
|
.read_to_end(&mut buf)?;
|
||||||
|
if buf.len() > INTO_STRING_LIMIT {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"response too big for into_string",
|
||||||
|
));
|
||||||
|
}
|
||||||
let (text, _, _) = encoding.decode(&buf);
|
let (text, _, _) = encoding.decode(&buf);
|
||||||
Ok(text.into_owned())
|
Ok(text.into_owned())
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "charset"))]
|
#[cfg(not(feature = "charset"))]
|
||||||
{
|
{
|
||||||
let mut buf: Vec<u8> = vec![];
|
let mut buf: Vec<u8> = vec![];
|
||||||
self.into_reader().read_to_end(&mut buf)?;
|
self.into_reader()
|
||||||
|
.take((INTO_STRING_LIMIT + 1) as u64)
|
||||||
|
.read_to_end(&mut buf)?;
|
||||||
|
if buf.len() > INTO_STRING_LIMIT {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"response too big for into_string",
|
||||||
|
));
|
||||||
|
}
|
||||||
Ok(String::from_utf8_lossy(&buf).to_string())
|
Ok(String::from_utf8_lossy(&buf).to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,7 +456,7 @@ impl Response {
|
|||||||
let (index, status) = parse_status_line(status_line.as_str())?;
|
let (index, status) = parse_status_line(status_line.as_str())?;
|
||||||
|
|
||||||
let mut headers: Vec<Header> = Vec::new();
|
let mut headers: Vec<Header> = Vec::new();
|
||||||
loop {
|
while headers.len() <= MAX_HEADER_COUNT {
|
||||||
let line = read_next_line(&mut stream, "a header")?;
|
let line = read_next_line(&mut stream, "a header")?;
|
||||||
if line.is_empty() {
|
if line.is_empty() {
|
||||||
break;
|
break;
|
||||||
@@ -437,6 +466,12 @@ impl Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if headers.len() > MAX_HEADER_COUNT {
|
||||||
|
return Err(ErrorKind::BadHeader.msg(
|
||||||
|
format!("more than {} header fields in response", MAX_HEADER_COUNT).as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Response {
|
Ok(Response {
|
||||||
url: None,
|
url: None,
|
||||||
status_line,
|
status_line,
|
||||||
@@ -549,9 +584,21 @@ impl FromStr for Response {
|
|||||||
|
|
||||||
fn read_next_line(reader: &mut impl BufRead, context: &str) -> io::Result<HeaderLine> {
|
fn read_next_line(reader: &mut impl BufRead, context: &str) -> io::Result<HeaderLine> {
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
let result = reader.read_until(b'\n', &mut buf);
|
let result = reader
|
||||||
|
.take((MAX_HEADER_SIZE + 1) as u64)
|
||||||
|
.read_until(b'\n', &mut buf);
|
||||||
|
|
||||||
if let Err(e) = result {
|
match result {
|
||||||
|
Ok(0) => Err(io::Error::new(
|
||||||
|
io::ErrorKind::ConnectionAborted,
|
||||||
|
"Unexpected EOF",
|
||||||
|
)),
|
||||||
|
Ok(n) if n > MAX_HEADER_SIZE => Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
format!("header field longer than {} bytes", MAX_HEADER_SIZE),
|
||||||
|
)),
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => {
|
||||||
// Provide context to errors encountered while reading the line.
|
// Provide context to errors encountered while reading the line.
|
||||||
let reason = format!("Error encountered in {}", context);
|
let reason = format!("Error encountered in {}", context);
|
||||||
|
|
||||||
@@ -561,15 +608,9 @@ fn read_next_line(reader: &mut impl BufRead, context: &str) -> io::Result<Header
|
|||||||
// as well as a .source() reference to the original error.
|
// as well as a .source() reference to the original error.
|
||||||
let wrapper = Error::new(ErrorKind::Io, Some(reason)).src(e);
|
let wrapper = Error::new(ErrorKind::Io, Some(reason)).src(e);
|
||||||
|
|
||||||
return Err(io::Error::new(kind, wrapper));
|
Err(io::Error::new(kind, wrapper))
|
||||||
}
|
|
||||||
|
|
||||||
if result? == 0 {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::ConnectionAborted,
|
|
||||||
"Unexpected EOF",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
}?;
|
||||||
|
|
||||||
if !buf.ends_with(b"\n") {
|
if !buf.ends_with(b"\n") {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
@@ -672,6 +713,8 @@ impl Read for ErrorReader {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -755,6 +798,25 @@ mod tests {
|
|||||||
assert_eq!("hello world!!!", resp.into_string().unwrap());
|
assert_eq!("hello world!!!", resp.into_string().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn into_string_large() {
|
||||||
|
const LEN: usize = INTO_STRING_LIMIT + 1;
|
||||||
|
let s = format!(
|
||||||
|
"HTTP/1.1 200 OK\r\n\
|
||||||
|
Content-Length: {}\r\n
|
||||||
|
\r\n
|
||||||
|
{}",
|
||||||
|
LEN,
|
||||||
|
"A".repeat(LEN),
|
||||||
|
);
|
||||||
|
let result = s.parse::<Response>().unwrap();
|
||||||
|
let err = result
|
||||||
|
.into_string()
|
||||||
|
.expect_err("didn't error with too-long body");
|
||||||
|
assert_eq!(err.to_string(), "response too big for into_string");
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::Other);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "json")]
|
#[cfg(feature = "json")]
|
||||||
fn parse_simple_json() {
|
fn parse_simple_json() {
|
||||||
@@ -801,6 +863,43 @@ mod tests {
|
|||||||
assert_eq!(resp.status_text(), "");
|
assert_eq!(resp.status_text(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_next_line_large() {
|
||||||
|
const LEN: usize = MAX_HEADER_SIZE + 1;
|
||||||
|
let s = format!("Long-Header: {}\r\n", "A".repeat(LEN),);
|
||||||
|
let mut cursor = Cursor::new(s);
|
||||||
|
let result = read_next_line(&mut cursor, "some context");
|
||||||
|
let err = result.expect_err("did not error on too-large header");
|
||||||
|
assert_eq!(err.kind(), io::ErrorKind::Other);
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
format!("header field longer than {} bytes", MAX_HEADER_SIZE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn too_many_headers() {
|
||||||
|
const LEN: usize = MAX_HEADER_COUNT + 1;
|
||||||
|
let s = format!(
|
||||||
|
"HTTP/1.1 200 OK\r\n\
|
||||||
|
{}
|
||||||
|
\r\n
|
||||||
|
hi",
|
||||||
|
"Header: value\r\n".repeat(LEN),
|
||||||
|
);
|
||||||
|
let err = s
|
||||||
|
.parse::<Response>()
|
||||||
|
.expect_err("did not error on too many headers");
|
||||||
|
assert_eq!(err.kind(), ErrorKind::BadHeader);
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
format!(
|
||||||
|
"Bad Header: more than {} header fields in response",
|
||||||
|
MAX_HEADER_COUNT
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "charset")]
|
#[cfg(feature = "charset")]
|
||||||
fn read_next_line_non_ascii_reason() {
|
fn read_next_line_non_ascii_reason() {
|
||||||
|
|||||||
Reference in New Issue
Block a user