Skip to content

Commit 904ed16

Browse files
committed
Merge #452: [bitreq] Fix unbounded proxy / body reads
103ed75 Feature-gate `proxy`-related error types to avoid unused warnings (Elias Rohrer) b49c4d4 [bitreq] Use saturating_add for chunked content-length accumulation (Elias Rohrer) fe9d7ad [bitreq] Apply max_body_size limit to lazily loaded responses (Elias Rohrer) 281ecbc [bitreq] Add configurable response body size limit (Elias Rohrer) 3e599f0 [bitreq] Limit response size in async proxy reads (Elias Rohrer) 48dd170 [bitreq] Limit response size in sync proxy reads (Elias Rohrer) Pull request description: Asked Claude to fix a few minor issues that might allow a malicious counterparty to send us unbounded data. (cc @TheBlueMatt) ACKs for top commit: tcharding: ACK 103ed75 Tree-SHA512: e21acc926980bde6e7865b254f2f7ea6d49026f3c0a6631b8b72cc0c1ff054a2bc3ee9254a3ee8e829091d4c999d2b88caf181dfdb110cd9485dced8284bb0c7
2 parents 00e31f6 + 103ed75 commit 904ed16

5 files changed

Lines changed: 105 additions & 11 deletions

File tree

bitreq/src/connection.rs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -322,12 +322,23 @@ impl AsyncConnection {
322322
tcp.write_all(proxy_request.as_bytes()).await?;
323323
tcp.flush().await?;
324324

325+
// Max proxy response size to prevent unbounded memory allocation
326+
const MAX_PROXY_RESPONSE_SIZE: usize = 16 * 1024;
325327
let mut proxy_response = Vec::new();
326-
let mut buf = vec![0; 256];
328+
let mut buf = [0; 256];
329+
327330
loop {
328331
let n = tcp.read(&mut buf).await?;
332+
if n == 0 {
333+
// EOF reached
334+
break;
335+
}
329336
proxy_response.extend_from_slice(&buf[..n]);
330-
if n < 256 {
337+
if proxy_response.len() > MAX_PROXY_RESPONSE_SIZE {
338+
return Err(Error::ProxyConnect);
339+
}
340+
if n < buf.len() {
341+
// Partial read indicates end of response
331342
break;
332343
}
333344
}
@@ -510,6 +521,7 @@ impl AsyncConnection {
510521
request.config.method == Method::Head,
511522
request.config.max_headers_size,
512523
request.config.max_status_line_len,
524+
request.config.max_body_size,
513525
)
514526
.await?;
515527

@@ -669,13 +681,23 @@ impl Connection {
669681
write!(tcp, "{}", proxy.connect(params.host, params.port.port()))?;
670682
tcp.flush()?;
671683

684+
// Max proxy response size to prevent unbounded memory allocation
685+
const MAX_PROXY_RESPONSE_SIZE: usize = 16 * 1024;
672686
let mut proxy_response = Vec::new();
687+
let mut buf = [0; 256];
673688

674689
loop {
675-
let mut buf = vec![0; 256];
676-
let total = tcp.read(&mut buf)?;
677-
proxy_response.append(&mut buf);
678-
if total < 256 {
690+
let n = tcp.read(&mut buf)?;
691+
if n == 0 {
692+
// EOF reached
693+
break;
694+
}
695+
proxy_response.extend_from_slice(&buf[..n]);
696+
if proxy_response.len() > MAX_PROXY_RESPONSE_SIZE {
697+
return Err(Error::ProxyConnect);
698+
}
699+
if n < buf.len() {
700+
// Partial read indicates end of response
679701
break;
680702
}
681703
}
@@ -707,6 +729,7 @@ impl Connection {
707729
self.stream,
708730
request.config.max_headers_size,
709731
request.config.max_status_line_len,
732+
request.config.max_body_size,
710733
)?;
711734
handle_redirects(request, response)
712735
})

bitreq/src/error.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,20 @@ pub enum Error {
6161
HttpsFeatureNotEnabled,
6262
/// The provided proxy information was not properly formatted. See
6363
/// [Proxy::new](crate::Proxy::new) for the valid format.
64+
#[cfg(feature = "proxy")]
6465
BadProxy,
6566
/// The provided credentials were rejected by the proxy server.
67+
#[cfg(feature = "proxy")]
6668
BadProxyCreds,
6769
/// The provided proxy credentials were malformed.
70+
#[cfg(feature = "proxy")]
6871
ProxyConnect,
6972
/// The provided credentials were rejected by the proxy server.
73+
#[cfg(feature = "proxy")]
7074
InvalidProxyCreds,
75+
/// The response body size surpasses
76+
/// [Request::with_max_body_size](crate::request::Request::with_max_body_size).
77+
BodyOverflow,
7178
// TODO: Uncomment these two for 3.0
7279
// /// The URL does not start with http:// or https://.
7380
// InvalidProtocol,
@@ -107,10 +114,15 @@ impl fmt::Display for Error {
107114
TooManyRedirections => write!(f, "too many redirections (over the max)"),
108115
InvalidUtf8InResponse => write!(f, "response contained invalid utf-8 where valid utf-8 was expected"),
109116
HttpsFeatureNotEnabled => write!(f, "request url contains https:// but the https feature is not enabled"),
117+
#[cfg(feature = "proxy")]
110118
BadProxy => write!(f, "the provided proxy information is malformed"),
119+
#[cfg(feature = "proxy")]
111120
BadProxyCreds => write!(f, "the provided proxy credentials are malformed"),
121+
#[cfg(feature = "proxy")]
112122
ProxyConnect => write!(f, "could not connect to the proxy server"),
123+
#[cfg(feature = "proxy")]
113124
InvalidProxyCreds => write!(f, "the provided proxy credentials are invalid"),
125+
BodyOverflow => write!(f, "the response body size surpassed max_body_size"),
114126
// TODO: Uncomment these two for 3.0
115127
// InvalidProtocol => write!(f, "the url does not start with http:// or https://"),
116128
// InvalidProtocolInRedirect => write!(f, "got redirected to an absolute url which does not start with http:// or https://"),

bitreq/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
//!
8484
//! ## `proxy`
8585
//!
86-
//! This feature enables HTTP proxy support. See [Proxy].
86+
//! This feature enables HTTP proxy support.
8787
//!
8888
//! # Examples
8989
//!

bitreq/src/request.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ pub struct Request {
9494
pub(crate) pipelining: bool,
9595
pub(crate) max_headers_size: Option<usize>,
9696
pub(crate) max_status_line_len: Option<usize>,
97+
pub(crate) max_body_size: Option<usize>,
9798
max_redirects: usize,
9899
#[cfg(feature = "proxy")]
99100
pub(crate) proxy: Option<Proxy>,
@@ -118,6 +119,7 @@ impl Request {
118119
pipelining: false,
119120
max_headers_size: None,
120121
max_status_line_len: None,
122+
max_body_size: None,
121123
max_redirects: 100,
122124
#[cfg(feature = "proxy")]
123125
proxy: None,
@@ -247,6 +249,23 @@ impl Request {
247249
self
248250
}
249251

252+
/// Sets the maximum size of the response body this request will
253+
/// accept.
254+
///
255+
/// If this limit is passed, the request will close the connection
256+
/// and return an [Error::BodyOverflow] error.
257+
///
258+
/// The maximum size is counted in bytes.
259+
///
260+
/// `None` disables the cap, and may cause the program to use any
261+
/// amount of memory if the server responds with a large (or
262+
/// infinite) body. The default is None, so setting this
263+
/// manually is recommended when talking to untrusted servers.
264+
pub fn with_max_body_size<S: Into<Option<usize>>>(mut self, max_body_size: S) -> Request {
265+
self.max_body_size = max_body_size.into();
266+
self
267+
}
268+
250269
/// Sets the proxy to use.
251270
#[cfg(feature = "proxy")]
252271
pub fn with_proxy(mut self, proxy: Proxy) -> Request {
@@ -282,10 +301,11 @@ impl Request {
282301
pub fn send(self) -> Result<Response, Error> {
283302
let parsed_request = ParsedRequest::new(self)?;
284303
let is_head = parsed_request.config.method == Method::Head;
304+
let max_body_size = parsed_request.config.max_body_size;
285305
let connection =
286306
Connection::new(parsed_request.connection_params(), parsed_request.timeout_at)?;
287307
let response = connection.send(parsed_request)?;
288-
Response::create(response, is_head)
308+
Response::create(response, is_head, max_body_size)
289309
}
290310

291311
/// Sends this request to the host, loaded lazily.

bitreq/src/response.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,18 @@ pub struct Response {
5252

5353
impl Response {
5454
#[cfg(feature = "std")]
55-
pub(crate) fn create(mut parent: ResponseLazy, is_head: bool) -> Result<Response, Error> {
55+
pub(crate) fn create(
56+
mut parent: ResponseLazy,
57+
is_head: bool,
58+
max_body_size: Option<usize>,
59+
) -> Result<Response, Error> {
5660
let mut body = Vec::new();
5761
if !is_head && parent.status_code != 204 && parent.status_code != 304 {
5862
for byte in &mut parent {
5963
let (byte, length) = byte?;
64+
if max_body_size.is_some_and(|max| body.len().saturating_add(length) > max) {
65+
return Err(Error::BodyOverflow);
66+
}
6067
body.reserve(length);
6168
body.push(byte);
6269
}
@@ -79,6 +86,7 @@ impl Response {
7986
is_head: bool,
8087
max_headers_size: Option<usize>,
8188
max_status_line_len: Option<usize>,
89+
max_body_size: Option<usize>,
8290
) -> Result<Response, Error> {
8391
use HttpStreamState::*;
8492

@@ -98,6 +106,10 @@ impl Response {
98106
EndOnClose => {
99107
while let Some(byte_result) = read_until_closed_async(&mut stream).await {
100108
let (byte, length) = byte_result?;
109+
if max_body_size.is_some_and(|max| body.len().saturating_add(length) > max)
110+
{
111+
return Err(Error::BodyOverflow);
112+
}
101113
body.reserve(length);
102114
body.push(byte);
103115
}
@@ -107,6 +119,11 @@ impl Response {
107119
read_with_content_length_async(&mut stream, &mut length).await
108120
{
109121
let (byte, expected_length) = byte_result?;
122+
if max_body_size
123+
.is_some_and(|max| body.len().saturating_add(expected_length) > max)
124+
{
125+
return Err(Error::BodyOverflow);
126+
}
110127
body.reserve(expected_length);
111128
body.push(byte);
112129
}
@@ -123,6 +140,10 @@ impl Response {
123140
.await
124141
{
125142
let (byte, length) = byte_result?;
143+
if max_body_size.is_some_and(|max| body.len().saturating_add(length) > max)
144+
{
145+
return Err(Error::BodyOverflow);
146+
}
126147
body.reserve(length);
127148
body.push(byte);
128149
},
@@ -290,6 +311,8 @@ pub struct ResponseLazy {
290311
stream: HttpStreamBytes,
291312
state: HttpStreamState,
292313
max_trailing_headers_size: Option<usize>,
314+
max_body_size: Option<usize>,
315+
bytes_read: usize,
293316
}
294317

295318
#[cfg(feature = "std")]
@@ -301,6 +324,7 @@ impl ResponseLazy {
301324
stream: HttpStream,
302325
max_headers_size: Option<usize>,
303326
max_status_line_len: Option<usize>,
327+
max_body_size: Option<usize>,
304328
) -> Result<ResponseLazy, Error> {
305329
let mut stream = BufReader::with_capacity(BACKING_READ_BUFFER_LENGTH, stream).bytes();
306330
let ResponseMetadata {
@@ -319,6 +343,8 @@ impl ResponseLazy {
319343
stream,
320344
state,
321345
max_trailing_headers_size,
346+
max_body_size,
347+
bytes_read: 0,
322348
})
323349
}
324350

@@ -333,6 +359,9 @@ impl ResponseLazy {
333359
stream: BufReader::with_capacity(1, http_stream).bytes(),
334360
state: HttpStreamState::EndOnClose,
335361
max_trailing_headers_size: None,
362+
// Body was already fully loaded and size-checked by send_async
363+
max_body_size: None,
364+
bytes_read: 0,
336365
}
337366
}
338367
}
@@ -343,7 +372,7 @@ impl Iterator for ResponseLazy {
343372

344373
fn next(&mut self) -> Option<Self::Item> {
345374
use HttpStreamState::*;
346-
match self.state {
375+
let result = match self.state {
347376
EndOnClose => read_until_closed(&mut self.stream),
348377
ContentLength(ref mut length) => read_with_content_length(&mut self.stream, length),
349378
Chunked(ref mut expecting_chunks, ref mut length, ref mut content_length) =>
@@ -355,7 +384,17 @@ impl Iterator for ResponseLazy {
355384
content_length,
356385
self.max_trailing_headers_size,
357386
),
387+
};
388+
389+
// Check body size limit before returning the byte
390+
if let Some(Ok((_, expected_length))) = &result {
391+
if self.max_body_size.is_some_and(|max| self.bytes_read + expected_length > max) {
392+
return Some(Err(Error::BodyOverflow));
393+
}
394+
self.bytes_read += 1;
358395
}
396+
397+
result
359398
}
360399
}
361400

@@ -537,7 +576,7 @@ macro_rules! define_read_methods {
537576
return None;
538577
}
539578
*chunk_length = incoming_length;
540-
*content_length += incoming_length;
579+
*content_length = content_length.saturating_add(incoming_length);
541580
}
542581

543582
if *chunk_length > 0 {

0 commit comments

Comments
 (0)