From f494dcdf1c61d20bff7e9d0dc08c7a9d8bc03545 Mon Sep 17 00:00:00 2001 From: spacebear Date: Wed, 21 May 2025 16:58:41 -0400 Subject: [PATCH] Enforce size limit in payjoin-cli Because these are unbounded allocations a malicious receiver could cause memory exhaustion for the sender. This approach uses a bytes_stream to avoid loading the entire buffer into memory before checking the size limit. --- payjoin-cli/Cargo.toml | 4 +++- payjoin-cli/src/app/v1.rs | 30 +++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/payjoin-cli/Cargo.toml b/payjoin-cli/Cargo.toml index 4ee35d419..ecee9ed87 100644 --- a/payjoin-cli/Cargo.toml +++ b/payjoin-cli/Cargo.toml @@ -32,6 +32,7 @@ bitcoincore-rpc = "0.19.0" clap = { version = "~4.0.32", features = ["derive"] } config = "0.13.3" env_logger = "0.9.0" +futures-util = "0.3" http-body-util = { version = "0.1", optional = true } hyper = { version = "1", features = ["http1", "server"], optional = true } hyper-rustls = { version = "0.26", optional = true } @@ -39,12 +40,13 @@ hyper-util = { version = "0.1", optional = true } log = "0.4.7" payjoin = { version = "0.23.0", default-features = false } rcgen = { version = "0.11.1", optional = true } -reqwest = { version = "0.12", default-features = false } +reqwest = { version = "0.12", default-features = false, features = ["stream"] } rustls = { version = "0.22.4", optional = true } serde = { version = "1.0.160", features = ["derive"] } sled = "0.34" tokio = { version = "1.38.1", features = ["full"] } tokio-rustls = { version = "0.25", features = ["ring"], default-features = false, optional = true } +tokio-util = { version = "0.7", features = ["io", "io-util"] } url = { version = "2.3.1", features = ["serde"] } [dev-dependencies] diff --git a/payjoin-cli/src/app/v1.rs b/payjoin-cli/src/app/v1.rs index 9ab6c9ba7..564edd32c 100644 --- a/payjoin-cli/src/app/v1.rs +++ b/payjoin-cli/src/app/v1.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use anyhow::{anyhow, Context, Result}; use bitcoincore_rpc::bitcoin::Amount; +use futures_util::TryStreamExt; use http_body_util::combinators::BoxBody; use http_body_util::{BodyExt, Full}; use hyper::body::{Buf, Bytes, Incoming}; @@ -17,9 +18,10 @@ use payjoin::bitcoin::FeeRate; use payjoin::receive::v1::{PayjoinProposal, UncheckedProposal}; use payjoin::receive::ReplyableError::{self, Implementation, V1}; use payjoin::send::v1::SenderBuilder; -use payjoin::{ImplementationError, Uri, UriExt}; +use payjoin::{ImplementationError, Uri, UriExt, MAX_CONTENT_LENGTH}; use tokio::net::TcpListener; use tokio::sync::watch; +use tokio_util::io::{StreamReader, SyncIoBridge}; use super::config::Config; use super::wallet::BitcoindWallet; @@ -88,12 +90,26 @@ impl AppTrait for App { "Sent fallback transaction hex: {:#}", payjoin::bitcoin::consensus::encode::serialize_hex(&fallback_tx) ); - let psbt = ctx.process_response(&mut response.bytes().await?.to_vec().as_slice()).map_err( - |e| { - log::debug!("Error processing response: {e:?}"); - anyhow!("Failed to process response {e}") - }, - )?; + + if let Some(content_length) = response.content_length() { + if content_length > MAX_CONTENT_LENGTH as u64 { + return Err(anyhow!( + "Response content length exceeded the limit of {MAX_CONTENT_LENGTH} bytes" + )); + } + } + + // Pass the response body to process_response without loading it all into memory. + // This prevents a maliciously crafted response from causing an unbounded allocation. + let stream = + response.bytes_stream().map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)); + let async_reader = StreamReader::new(stream); + let mut sync_reader = SyncIoBridge::new(async_reader); + + let psbt = ctx.process_response(&mut sync_reader).map_err(|e| { + log::debug!("Error processing response: {e:?}"); + anyhow!("Failed to process response {}", e) + })?; self.process_pj_response(psbt)?; Ok(())