Skip to content

Conversation

@spacebear21
Copy link
Collaborator

@spacebear21 spacebear21 commented May 21, 2025

This diff was previously included in #586 but I deleted it by mistake while rebasing on top of master and resolving conflicts.

@coveralls
Copy link
Collaborator

coveralls commented May 21, 2025

Pull Request Test Coverage Report for Build 15172527684

Details

  • 5 of 8 (62.5%) changed or added relevant lines in 1 file are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-0.009%) to 83.516%

Changes Missing Coverage Covered Lines Changed/Added Lines %
payjoin-cli/src/app/v1.rs 5 8 62.5%
Totals Coverage Status
Change from base Build 15169328807: -0.009%
Covered Lines: 5948
Relevant Lines: 7122

💛 - Coveralls

Copy link
Contributor

@shinghim shinghim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 0734972

Copy link
Collaborator

@nothingmuch nothingmuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as implemented this check is redundant with the payjoin crate's, since it does not protect against allocation failures

Comment on lines 91 to 92
let response_bytes = response.bytes().await?;
if response_bytes.len() > MAX_CONTENT_LENGTH {
Copy link
Collaborator

@nothingmuch nothingmuch May 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if a sufficiently large response is sent then the program might still crash, because first the excessively sized buffer would be allocated (in BytesExt::collect), and only then is its length is compared to the limit.

i think the way to address this is:

  1. BIP 78 does not specify that content-length is required on the response, if it's set check that it does not exceed the maximum
  2. read up to MAX_CONTENT_LENGTH bytes into a buffer, failing if the limit is exceeded and data is still available to read

it seems like response.bytes_stream() could be used instead of response.bytes()

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.
Comment on lines +102 to +112
// 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)
})?;
Copy link
Collaborator Author

@spacebear21 spacebear21 May 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nothingmuch Does this approach look sane to you? I had to get LLM help to convert the bytes_stream to std::io::Read for process_response, and this adds a few new dependencies.

EDIT: hmm it worked fine when I smoke tested the payjoin-cli receive/send flow but the e2e tests appear to disagree.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach seems correct, avoids duplication of knowledge about the limits, and just trusts the payjoin crate to enforce the limit.

It appears SyncIoBridge requires a nested tokio runtime but that isn't allowed, perhaps in relation to how async tests are set up? That seems a bit brittle but if we can work around it perfectly fine since this is just a payjoin-cli quirk...

If it's simpler, then we can also just buffer twice, or even explicitly require buffering by changing process_response to take a &[u8] instead of a &mut impl std::io::Read, because in either case it would still be easy to have memory exhaustion vulnerable code calling this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am now leaning towards "changing process_response to take a &[u8] instead of a &mut impl std::io::Read", and I think we should do this for the UncheckedProposal::from_request body parameter as well.

  1. I'm not a big fan of the extra complexity/dependencies introduced by this StreamReader/SyncIoBridge approach.
  2. Like you said, the app implementer needs to be careful either way to avoid OOM bugs.
  3. We use &[u8] for the V2 Sender and Receivers process_response methods.

@spacebear21 spacebear21 requested a review from nothingmuch May 22, 2025 20:41
@spacebear21 spacebear21 marked this pull request as draft May 22, 2025 20:43
spacebear21 added a commit that referenced this pull request Jun 9, 2025
This was prompted by the discussion in
#710 (comment).

Using a reader still left callers susceptible to writing memory
exhaustion vulnerable code, and just introduced extra complexity by
having to buffer twice. We can recognize this and leave the buffering
entirely up to the caller. Also, the V2 sender and receiver response
processing functions already take &[u8], so this makes the function
signatures more consistent across versions.
@nothingmuch
Copy link
Collaborator

this has been obsoleted by #745 right?

@spacebear21
Copy link
Collaborator Author

this has been obsoleted by #745 right?

Well the point of this PR was to prevent buffer overflows in payjoin-cli, which #745 doesn't prevent. But the current approach taken in this PR is indeed obsolete. payjoin-cli should now just "read up to MAX_CONTENT_LENGTH bytes into a buffer, failing if the limit is exceeded and data is still available to read" as you suggested earlier in this PR. And depending on your thoughts re: #770 (comment), maybe we should also move MAX_CONTENT_LENGTH to live only in payjoin-cli?

@nothingmuch
Copy link
Collaborator

Well the point of this PR was to prevent buffer overflows in payjoin-cli, which #745 doesn't prevent.

yes, good point!

maybe we should also move MAX_CONTENT_LENGTH to live only in payjoin-cli?

yeah that sounds right to me

@spacebear21
Copy link
Collaborator Author

Superseded by #808

node-smithxby72w added a commit to node-smithxby72w/rust-payjoin that referenced this pull request Sep 28, 2025
This was prompted by the discussion in
payjoin/rust-payjoin#710 (comment).

Using a reader still left callers susceptible to writing memory
exhaustion vulnerable code, and just introduced extra complexity by
having to buffer twice. We can recognize this and leave the buffering
entirely up to the caller. Also, the V2 sender and receiver response
processing functions already take &[u8], so this makes the function
signatures more consistent across versions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants