Skip to content

Commit 465c1dc

Browse files
committed
Cap the ohttp key body size
This caps the key cofig fetch size from the directory to limit any malicious directory from sending oversized data on key fetch.
1 parent 4590ab2 commit 465c1dc

1 file changed

Lines changed: 61 additions & 1 deletion

File tree

payjoin/src/core/io.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ use reqwest::{Client, Proxy};
77
use crate::into_url::IntoUrl;
88
use crate::OhttpKeys;
99

10+
/// Upper bound on the size of an OHTTP key configuration response body.
11+
///
12+
/// Derived from the ECHKeyConfig wire format: `key_id(1) + kem_id(2) +
13+
/// K-256 public key(65) + cipher suite vector length(2) + cipher suites` where
14+
/// the suite vector is u16-length-bounded (at most 65532 bytes of suites). Any
15+
/// larger response cannot decode and is rejected before being fully buffered
16+
/// to prevent memory exhaustion from a hostile payjoin directory.
17+
pub const MAX_OHTTP_KEYS_BODY_LEN: usize = 1 + 2 + 65 + 2 + u16::MAX as usize - 3;
18+
1019
/// Fetch the ohttp keys from the specified payjoin directory via proxy.
1120
///
1221
/// * `ohttp_relay`: The http CONNECT method proxy to request the ohttp keys from a payjoin
@@ -69,7 +78,21 @@ async fn parse_ohttp_keys_response(res: reqwest::Response) -> Result<OhttpKeys,
6978
return Err(Error::UnexpectedStatusCode(res.status()));
7079
}
7180

72-
let body = res.bytes().await?.to_vec();
81+
if let Some(len) = res.content_length() {
82+
if len as usize > MAX_OHTTP_KEYS_BODY_LEN {
83+
return Err(Error::OhttpKeysBodyTooLarge(len));
84+
}
85+
}
86+
87+
let mut body = Vec::with_capacity(MAX_OHTTP_KEYS_BODY_LEN);
88+
let mut res = res;
89+
while let Some(chunk) = res.chunk().await? {
90+
body.extend_from_slice(&chunk);
91+
if body.len() > MAX_OHTTP_KEYS_BODY_LEN {
92+
return Err(Error::OhttpKeysBodyTooLarge(body.len() as u64));
93+
}
94+
}
95+
7396
OhttpKeys::decode(&body).map_err(|e| {
7497
Error::Internal(InternalError(InternalErrorInner::InvalidOhttpKeys(e.to_string())))
7598
})
@@ -80,6 +103,9 @@ async fn parse_ohttp_keys_response(res: reqwest::Response) -> Result<OhttpKeys,
80103
pub enum Error {
81104
/// When the payjoin directory returns an unexpected status code
82105
UnexpectedStatusCode(http::StatusCode),
106+
/// When the payjoin directory returns an OHTTP key configuration body
107+
/// larger than [`MAX_OHTTP_KEYS_BODY_LEN`]
108+
OhttpKeysBodyTooLarge(u64),
83109
/// Internal errors that should not be pattern matched by users
84110
#[doc(hidden)]
85111
Internal(InternalError),
@@ -126,6 +152,10 @@ impl std::fmt::Display for Error {
126152
Self::UnexpectedStatusCode(code) => {
127153
write!(f, "Unexpected status code from payjoin directory: {code}")
128154
}
155+
Self::OhttpKeysBodyTooLarge(len) => write!(
156+
f,
157+
"OHTTP keys body of {len} bytes exceeds the maximum of {MAX_OHTTP_KEYS_BODY_LEN} bytes"
158+
),
129159
Self::Internal(InternalError(e)) => e.fmt(f),
130160
}
131161
}
@@ -153,6 +183,7 @@ impl std::error::Error for Error {
153183
match self {
154184
Self::Internal(InternalError(e)) => e.source(),
155185
Self::UnexpectedStatusCode(_) => None,
186+
Self::OhttpKeysBodyTooLarge(_) => None,
156187
}
157188
}
158189
}
@@ -234,4 +265,33 @@ mod tests {
234265
"expected InvalidOhttpKeys error"
235266
);
236267
}
268+
269+
#[tokio::test]
270+
async fn test_max_body_len_boundary() {
271+
// number is literal pin of MAX_OHTTP_KEYS_BODY_LEN
272+
let response = mock_response(StatusCode::OK, vec![0u8; 65602]);
273+
assert!(
274+
matches!(
275+
parse_ohttp_keys_response(response).await,
276+
Err(Error::Internal(InternalError(InternalErrorInner::InvalidOhttpKeys(_))))
277+
),
278+
"body of exactly MAX_OHTTP_KEYS_BODY_LEN must not be rejected as oversized"
279+
);
280+
}
281+
282+
#[tokio::test]
283+
async fn test_parse_oversized_body_without_content_length() {
284+
// number is literal pin of MAX_OHTTP_KEYS_BODY_LEN
285+
let oversized_body = vec![0u8; 65602 + 1];
286+
287+
let response = mock_response(StatusCode::OK, oversized_body);
288+
289+
assert!(
290+
matches!(
291+
parse_ohttp_keys_response(response).await,
292+
Err(Error::OhttpKeysBodyTooLarge(_))
293+
),
294+
"expected OhttpKeysBodyTooLarge error"
295+
);
296+
}
237297
}

0 commit comments

Comments
 (0)