Skip to content

Commit bdf0004

Browse files
committed
Enforce content-length validation on sender and size limits on payjoin-cli
1 parent 8efe975 commit bdf0004

File tree

10 files changed

+279
-66
lines changed

10 files changed

+279
-66
lines changed

Cargo-minimal.lock

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2569,6 +2569,7 @@ dependencies = [
25692569
"hyper",
25702570
"hyper-rustls",
25712571
"hyper-util",
2572+
"log",
25722573
"nix",
25732574
"payjoin",
25742575
"payjoin-test-utils",
@@ -2577,6 +2578,7 @@ dependencies = [
25772578
"rcgen",
25782579
"reqwest",
25792580
"rusqlite",
2581+
"rustls 0.22.4",
25802582
"serde",
25812583
"serde_json",
25822584
"sled",
@@ -3155,6 +3157,7 @@ dependencies = [
31553157
"base64 0.22.1",
31563158
"bytes",
31573159
"futures-core",
3160+
"futures-util",
31583161
"http",
31593162
"http-body",
31603163
"http-body-util",
@@ -3175,12 +3178,14 @@ dependencies = [
31753178
"sync_wrapper",
31763179
"tokio",
31773180
"tokio-rustls",
3181+
"tokio-util",
31783182
"tower",
31793183
"tower-http",
31803184
"tower-service",
31813185
"url",
31823186
"wasm-bindgen",
31833187
"wasm-bindgen-futures",
3188+
"wasm-streams",
31843189
"web-sys",
31853190
"webpki-roots 1.0.2",
31863191
]
@@ -3287,6 +3292,20 @@ dependencies = [
32873292
"sct",
32883293
]
32893294

3295+
[[package]]
3296+
name = "rustls"
3297+
version = "0.22.4"
3298+
source = "registry+https://github.com/rust-lang/crates.io-index"
3299+
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
3300+
dependencies = [
3301+
"log",
3302+
"ring",
3303+
"rustls-pki-types",
3304+
"rustls-webpki 0.102.8",
3305+
"subtle",
3306+
"zeroize",
3307+
]
3308+
32903309
[[package]]
32913310
name = "rustls"
32923311
version = "0.23.31"
@@ -3344,6 +3363,17 @@ dependencies = [
33443363
"untrusted",
33453364
]
33463365

3366+
[[package]]
3367+
name = "rustls-webpki"
3368+
version = "0.102.8"
3369+
source = "registry+https://github.com/rust-lang/crates.io-index"
3370+
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
3371+
dependencies = [
3372+
"ring",
3373+
"rustls-pki-types",
3374+
"untrusted",
3375+
]
3376+
33473377
[[package]]
33483378
name = "rustls-webpki"
33493379
version = "0.103.4"
@@ -4682,12 +4712,13 @@ dependencies = [
46824712

46834713
[[package]]
46844714
name = "wasm-bindgen-futures"
4685-
version = "0.4.43"
4715+
version = "0.4.50"
46864716
source = "registry+https://github.com/rust-lang/crates.io-index"
4687-
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
4717+
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
46884718
dependencies = [
46894719
"cfg-if",
46904720
"js-sys",
4721+
"once_cell",
46914722
"wasm-bindgen",
46924723
"web-sys",
46934724
]
@@ -4724,11 +4755,24 @@ dependencies = [
47244755
"unicode-ident",
47254756
]
47264757

4758+
[[package]]
4759+
name = "wasm-streams"
4760+
version = "0.4.2"
4761+
source = "registry+https://github.com/rust-lang/crates.io-index"
4762+
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
4763+
dependencies = [
4764+
"futures-util",
4765+
"js-sys",
4766+
"wasm-bindgen",
4767+
"wasm-bindgen-futures",
4768+
"web-sys",
4769+
]
4770+
47274771
[[package]]
47284772
name = "web-sys"
4729-
version = "0.3.70"
4773+
version = "0.3.77"
47304774
source = "registry+https://github.com/rust-lang/crates.io-index"
4731-
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
4775+
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
47324776
dependencies = [
47334777
"js-sys",
47344778
"wasm-bindgen",

Cargo-recent.lock

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2569,6 +2569,7 @@ dependencies = [
25692569
"hyper",
25702570
"hyper-rustls",
25712571
"hyper-util",
2572+
"log",
25722573
"nix",
25732574
"payjoin",
25742575
"payjoin-test-utils",
@@ -2577,6 +2578,7 @@ dependencies = [
25772578
"rcgen",
25782579
"reqwest",
25792580
"rusqlite",
2581+
"rustls 0.22.4",
25802582
"serde",
25812583
"serde_json",
25822584
"sled",
@@ -3155,6 +3157,7 @@ dependencies = [
31553157
"base64 0.22.1",
31563158
"bytes",
31573159
"futures-core",
3160+
"futures-util",
31583161
"http",
31593162
"http-body",
31603163
"http-body-util",
@@ -3175,12 +3178,14 @@ dependencies = [
31753178
"sync_wrapper",
31763179
"tokio",
31773180
"tokio-rustls",
3181+
"tokio-util",
31783182
"tower",
31793183
"tower-http",
31803184
"tower-service",
31813185
"url",
31823186
"wasm-bindgen",
31833187
"wasm-bindgen-futures",
3188+
"wasm-streams",
31843189
"web-sys",
31853190
"webpki-roots 1.0.2",
31863191
]
@@ -3287,6 +3292,20 @@ dependencies = [
32873292
"sct",
32883293
]
32893294

3295+
[[package]]
3296+
name = "rustls"
3297+
version = "0.22.4"
3298+
source = "registry+https://github.com/rust-lang/crates.io-index"
3299+
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
3300+
dependencies = [
3301+
"log",
3302+
"ring",
3303+
"rustls-pki-types",
3304+
"rustls-webpki 0.102.8",
3305+
"subtle",
3306+
"zeroize",
3307+
]
3308+
32903309
[[package]]
32913310
name = "rustls"
32923311
version = "0.23.31"
@@ -3344,6 +3363,17 @@ dependencies = [
33443363
"untrusted",
33453364
]
33463365

3366+
[[package]]
3367+
name = "rustls-webpki"
3368+
version = "0.102.8"
3369+
source = "registry+https://github.com/rust-lang/crates.io-index"
3370+
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
3371+
dependencies = [
3372+
"ring",
3373+
"rustls-pki-types",
3374+
"untrusted",
3375+
]
3376+
33473377
[[package]]
33483378
name = "rustls-webpki"
33493379
version = "0.103.4"
@@ -4682,12 +4712,13 @@ dependencies = [
46824712

46834713
[[package]]
46844714
name = "wasm-bindgen-futures"
4685-
version = "0.4.43"
4715+
version = "0.4.50"
46864716
source = "registry+https://github.com/rust-lang/crates.io-index"
4687-
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
4717+
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
46884718
dependencies = [
46894719
"cfg-if",
46904720
"js-sys",
4721+
"once_cell",
46914722
"wasm-bindgen",
46924723
"web-sys",
46934724
]
@@ -4724,11 +4755,24 @@ dependencies = [
47244755
"unicode-ident",
47254756
]
47264757

4758+
[[package]]
4759+
name = "wasm-streams"
4760+
version = "0.4.2"
4761+
source = "registry+https://github.com/rust-lang/crates.io-index"
4762+
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
4763+
dependencies = [
4764+
"futures-util",
4765+
"js-sys",
4766+
"wasm-bindgen",
4767+
"wasm-bindgen-futures",
4768+
"web-sys",
4769+
]
4770+
47274771
[[package]]
47284772
name = "web-sys"
4729-
version = "0.3.70"
4773+
version = "0.3.77"
47304774
source = "registry+https://github.com/rust-lang/crates.io-index"
4731-
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
4775+
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
47324776
dependencies = [
47334777
"js-sys",
47344778
"wasm-bindgen",

payjoin-cli/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ default = ["v2"]
2323
native-certs = ["reqwest/rustls-tls-native-roots"]
2424
_manual-tls = ["rcgen", "reqwest/rustls-tls", "hyper-rustls", "payjoin/_manual-tls", "tokio-rustls"]
2525
v1 = ["payjoin/v1","hyper", "hyper-util", "http-body-util"]
26-
v2 = ["payjoin/v2", "payjoin/io"]
26+
v2 = ["payjoin/v2", "payjoin/io", "hyper"]
2727

2828
[dependencies]
2929
anyhow = "1.0.99"
@@ -40,7 +40,7 @@ payjoin = { version = "0.24.0", default-features = false }
4040
r2d2 = "0.8.10"
4141
r2d2_sqlite = "0.22.0"
4242
rcgen = { version = "0.14.3", optional = true }
43-
reqwest = { version = "0.12.23", default-features = false, features = ["json", "rustls-tls"] }
43+
reqwest = { version = "0.12.23", default-features = false, features = ["json", "rustls-tls", "stream"] }
4444
rusqlite = { version = "0.29.0", features = ["bundled"] }
4545
serde_json = "1.0.142"
4646
serde = { version = "1.0.219", features = ["derive"] }
@@ -51,6 +51,8 @@ url = { version = "2.5.4", features = ["serde"] }
5151
dirs = "6.0.0"
5252
tracing = "0.1.41"
5353
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
54+
rustls = { version = "0.22.4", optional = true }
55+
log = "0.4.27"
5456

5557
[dev-dependencies]
5658
nix = { version = "0.30.1", features = ["aio", "process", "signal"] }

payjoin-cli/src/app/mod.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
use std::collections::HashMap;
22

3+
#[cfg(feature = "v1")]
4+
use anyhow::anyhow;
35
use anyhow::Result;
6+
#[cfg(feature = "v1")]
7+
use futures::{Stream, StreamExt};
8+
#[cfg(feature = "v1")]
9+
use hyper::body::Bytes;
410
use payjoin::bitcoin::psbt::Psbt;
511
use payjoin::bitcoin::{self, Address, Amount, FeeRate};
612
use tokio::signal;
@@ -82,3 +88,22 @@ async fn handle_interrupt(tx: watch::Sender<()>) {
8288
}
8389
let _ = tx.send(());
8490
}
91+
92+
#[cfg(feature = "v1")]
93+
pub async fn read_limited_body<S, E>(mut stream: S, expected_len: usize) -> Result<Vec<u8>>
94+
where
95+
S: Stream<Item = Result<Bytes, E>> + Unpin,
96+
E: std::error::Error + Send + Sync + 'static,
97+
{
98+
let mut body = Vec::with_capacity(expected_len);
99+
100+
while let Some(chunk) = stream.next().await {
101+
let chunk = chunk.map_err(|e| anyhow!("Error reading body chunk: {}", e))?;
102+
if body.len() + chunk.len() > expected_len {
103+
return Err(anyhow!("Body exceeds expected size of {expected_len} bytes"));
104+
}
105+
body.extend_from_slice(&chunk);
106+
}
107+
108+
Ok(body)
109+
}

payjoin-cli/src/app/v1.rs

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@ use tokio::sync::watch;
2323
use super::config::Config;
2424
use super::wallet::BitcoindWallet;
2525
use super::App as AppTrait;
26-
use crate::app::{handle_interrupt, http_agent};
26+
use crate::app::{handle_interrupt, http_agent, read_limited_body};
2727
use crate::db::Database;
2828

29+
/// 4M block size limit with base64 encoding overhead => maximum reasonable size of content-length
30+
/// 4_000_000 * 4 / 3 fits in u32
31+
const MAX_CONTENT_LENGTH: usize = 4_000_000 * 4 / 3;
32+
33+
#[derive(Clone)]
2934
struct Headers<'a>(&'a hyper::HeaderMap);
3035
impl payjoin::receive::v1::Headers for Headers<'_> {
3136
fn get_header(&self, key: &str) -> Option<&str> {
@@ -86,8 +91,22 @@ impl AppTrait for App {
8691
"Sent fallback transaction hex: {:#}",
8792
payjoin::bitcoin::consensus::encode::serialize_hex(&fallback_tx)
8893
);
89-
let psbt = ctx.process_response(&response.bytes().await?).map_err(|e| {
90-
tracing::debug!("Error processing response: {e:?}");
94+
95+
let expected_length = response
96+
.headers()
97+
.get("Content-Length")
98+
.and_then(|val| val.to_str().ok())
99+
.and_then(|s| s.parse::<usize>().ok())
100+
.unwrap_or(MAX_CONTENT_LENGTH);
101+
102+
if expected_length > MAX_CONTENT_LENGTH {
103+
return Err(anyhow!("Response body is too large: {} bytes", expected_length));
104+
}
105+
106+
let body = read_limited_body(response.bytes_stream(), MAX_CONTENT_LENGTH).await?;
107+
108+
let psbt = ctx.process_response(&body).map_err(|e| {
109+
log::debug!("Error processing response: {e:?}");
91110
anyhow!("Failed to process response {e}")
92111
})?;
93112

@@ -295,12 +314,26 @@ impl App {
295314
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, ReplyableError> {
296315
let (parts, body) = req.into_parts();
297316
let headers = Headers(&parts.headers);
298-
let query_string = parts.uri.query().unwrap_or("");
299-
let body = body
300-
.collect()
317+
318+
let expected_length = headers
319+
.0
320+
.get("Content-Length")
321+
.and_then(|val| val.to_str().ok())
322+
.and_then(|s| s.parse::<usize>().ok())
323+
.unwrap_or(MAX_CONTENT_LENGTH);
324+
325+
if expected_length > MAX_CONTENT_LENGTH {
326+
log::error!("Error: Content length exceeds max allowed");
327+
return Err(Implementation(ImplementationError::from(
328+
anyhow!("Content length too large: {expected_length}").into_boxed_dyn_error(),
329+
)));
330+
}
331+
332+
let body = read_limited_body(body.into_data_stream(), expected_length)
301333
.await
302-
.map_err(|e| Implementation(ImplementationError::new(e)))?
303-
.to_bytes();
334+
.map_err(|e| Implementation(ImplementationError::from(e.into_boxed_dyn_error())))?;
335+
336+
let query_string = parts.uri.query().unwrap_or("");
304337
let proposal = UncheckedOriginalPayload::from_request(&body, query_string, headers)?;
305338

306339
let payjoin_proposal = self.process_v1_proposal(proposal)?;

payjoin-cli/src/app/v2/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ impl AppTrait for App {
7171
let psbt = self.create_original_psbt(&address, amount, fee_rate)?;
7272
let (req, ctx) = payjoin::send::v1::SenderBuilder::from_parts(
7373
psbt,
74-
pj_param,
74+
&PjParam::V1(pj_param.clone()),
7575
&address,
7676
Some(amount),
7777
)

0 commit comments

Comments
 (0)