Skip to content

Commit 7c9e7ee

Browse files
authored
Limit response sizes for v1 (payjoin#586)
Add content length checks in v1 send and payjoin-cli. Fixes payjoin#483
2 parents c4bc5ce + 29ffade commit 7c9e7ee

File tree

5 files changed

+74
-10
lines changed

5 files changed

+74
-10
lines changed

payjoin-test-utils/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,6 @@ pub const KEM: Kem = Kem::K256Sha256;
267267
pub const SYMMETRIC: &[SymmetricSuite] =
268268
&[ohttp::SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305)];
269269

270-
// OriginalPSBT Test Vector from BIP 78
271270
// https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-span_idtestvectorsspanTest_vectors
272271
// | InputScriptType | Original PSBT Fee rate | maxadditionalfeecontribution | additionalfeeoutputindex|
273272
// |-----------------|-----------------------|------------------------------|-------------------------|

payjoin/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,7 @@ pub(crate) mod error_codes;
7676
pub use crate::core::error::ImplementationError;
7777
#[cfg(feature = "_core")]
7878
pub(crate) use crate::core::version::Version;
79+
80+
/// 4M block size limit with base64 encoding overhead => maximum reasonable size of content-length
81+
/// 4_000_000 * 4 / 3 fits in u32
82+
pub const MAX_CONTENT_LENGTH: usize = 4_000_000 * 4 / 3;

payjoin/src/receive/v1/exclusive/mod.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@ pub use error::RequestError;
44

55
use super::*;
66
use crate::into_url::IntoUrl;
7-
use crate::Version;
7+
use crate::{Version, MAX_CONTENT_LENGTH};
88

9-
/// 4_000_000 * 4 / 3 fits in u32
10-
const MAX_CONTENT_LENGTH: usize = 4_000_000 * 4 / 3;
119
const SUPPORTED_VERSIONS: &[Version] = &[Version::One];
1210

1311
pub trait Headers {

payjoin/src/send/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use bitcoin::transaction::Version;
66
use bitcoin::Sequence;
77

88
use crate::error_codes::ErrorCode;
9+
use crate::MAX_CONTENT_LENGTH;
910

1011
/// Error building a Sender from a SenderBuilder.
1112
///
@@ -95,6 +96,7 @@ pub struct ValidationError(InternalValidationError);
9596
pub(crate) enum InternalValidationError {
9697
Parse,
9798
Io(std::io::Error),
99+
ContentTooLarge,
98100
Proposal(InternalProposalError),
99101
#[cfg(feature = "v2")]
100102
V2Encapsulation(crate::send::v2::EncapsulationError),
@@ -119,6 +121,7 @@ impl fmt::Display for ValidationError {
119121
match &self.0 {
120122
Parse => write!(f, "couldn't decode as PSBT or JSON",),
121123
Io(e) => write!(f, "couldn't read PSBT: {e}"),
124+
ContentTooLarge => write!(f, "content is larger than {MAX_CONTENT_LENGTH} bytes"),
122125
Proposal(e) => write!(f, "proposal PSBT error: {e}"),
123126
#[cfg(feature = "v2")]
124127
V2Encapsulation(e) => write!(f, "v2 encapsulation error: {e}"),
@@ -133,6 +136,7 @@ impl std::error::Error for ValidationError {
133136
match &self.0 {
134137
Parse => None,
135138
Io(error) => Some(error),
139+
ContentTooLarge => None,
136140
Proposal(e) => Some(e),
137141
#[cfg(feature = "v2")]
138142
V2Encapsulation(e) => Some(e),

payjoin/src/send/v1.rs

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
//! [`bitmask-core`](https://github.com/diba-io/bitmask-core) BDK integration. Bring your own
2222
//! wallet and http client.
2323
24+
use std::io::{BufRead, BufReader};
25+
2426
use bitcoin::psbt::Psbt;
2527
use bitcoin::{FeeRate, ScriptBuf, Weight};
2628
use error::{BuildSenderError, InternalBuildSenderError};
@@ -30,7 +32,7 @@ use super::*;
3032
pub use crate::output_substitution::OutputSubstitution;
3133
use crate::psbt::PsbtExt;
3234
use crate::request::Request;
33-
pub use crate::PjUri;
35+
use crate::{PjUri, MAX_CONTENT_LENGTH};
3436

3537
/// A builder to construct the properties of a `Sender`.
3638
#[derive(Clone)]
@@ -277,19 +279,25 @@ impl V1Context {
277279
self,
278280
response: &mut impl std::io::Read,
279281
) -> Result<Psbt, ResponseError> {
280-
let mut res_str = String::new();
281-
response.read_to_string(&mut res_str).map_err(InternalValidationError::Io)?;
282-
let proposal = Psbt::from_str(&res_str).map_err(|_| ResponseError::parse(&res_str))?;
282+
let mut buf_reader = BufReader::with_capacity(MAX_CONTENT_LENGTH + 1, response);
283+
let buffer = buf_reader.fill_buf().map_err(InternalValidationError::Io)?;
284+
285+
if buffer.len() > MAX_CONTENT_LENGTH {
286+
return Err(ResponseError::from(InternalValidationError::ContentTooLarge));
287+
}
288+
289+
let res_str = std::str::from_utf8(buffer).map_err(|_| InternalValidationError::Parse)?;
290+
let proposal = Psbt::from_str(res_str).map_err(|_| ResponseError::parse(res_str))?;
283291
self.psbt_context.process_proposal(proposal).map_err(Into::into)
284292
}
285293
}
286294

287295
#[cfg(test)]
288296
mod test {
289297
use bitcoin::FeeRate;
290-
use payjoin_test_utils::{BoxError, PARSED_ORIGINAL_PSBT};
298+
use payjoin_test_utils::{BoxError, INVALID_PSBT, PARSED_ORIGINAL_PSBT, PAYJOIN_PROPOSAL};
291299

292-
use super::SenderBuilder;
300+
use super::*;
293301
use crate::error_codes::ErrorCode;
294302
use crate::send::error::{ResponseError, WellKnownError};
295303
use crate::send::test::create_psbt_context;
@@ -345,4 +353,55 @@ mod test {
345353
_ => panic!("Expected unrecognized JSON error"),
346354
}
347355
}
356+
357+
#[test]
358+
fn process_response_valid() {
359+
let mut cursor = std::io::Cursor::new(PAYJOIN_PROPOSAL.as_bytes());
360+
361+
let ctx = create_v1_context();
362+
let response = ctx.process_response(&mut cursor);
363+
assert!(response.is_ok())
364+
}
365+
366+
#[test]
367+
fn process_response_invalid_psbt() {
368+
let mut cursor = std::io::Cursor::new(INVALID_PSBT.as_bytes());
369+
370+
let ctx = create_v1_context();
371+
let response = ctx.process_response(&mut cursor);
372+
match response {
373+
Ok(_) => panic!("Invalid PSBT should have caused an error"),
374+
Err(error) => match error {
375+
ResponseError::Validation(e) => {
376+
assert_eq!(
377+
e.to_string(),
378+
ValidationError::from(InternalValidationError::Parse).to_string()
379+
);
380+
}
381+
_ => panic!("Unexpected error type"),
382+
},
383+
}
384+
}
385+
386+
#[test]
387+
fn process_response_invalid_utf8() {
388+
// In UTF-8, 0xF0 represents the start of a 4-byte sequence, so 0xF0 by itself is invalid
389+
let invalid_utf8 = [0xF0];
390+
let mut cursor = std::io::Cursor::new(invalid_utf8);
391+
392+
let ctx = create_v1_context();
393+
let response = ctx.process_response(&mut cursor);
394+
match response {
395+
Ok(_) => panic!("Invalid UTF-8 should have caused an error"),
396+
Err(error) => match error {
397+
ResponseError::Validation(e) => {
398+
assert_eq!(
399+
e.to_string(),
400+
ValidationError::from(InternalValidationError::Parse).to_string()
401+
);
402+
}
403+
_ => panic!("Unexpected error type"),
404+
},
405+
}
406+
}
348407
}

0 commit comments

Comments
 (0)