Skip to content

Reject userinfo and malformed ports in Url::parse - #1896

Merged
DanGould merged 1 commit into
payjoin:masterfrom
DanGould:url-userinfo-host-confusion
Sep 22, 2026
Merged

DanGould merged 1 commit into
payjoin:masterfrom
DanGould:url-userinfo-host-confusion

Conversation

@DanGould

@DanGould DanGould commented Sep 22, 2026

Copy link
Copy Markdown
Member

Url::parse had no notion of RFC 3986 userinfo. The host ended at the first : and any error from the port parser was discarded, so https://trusted.com:1234@evil.com/pj parsed with host trusted.com and the tail :1234@evil.com stored as the path and re-serialised verbatim. domain() and host_str() then disagreed with as_str(). A standards-compliant client such as reqwest reads that authority as userinfo trusted.com:1234 and host evil.com.

The v1 endpoint check consults domain() to allow plain http for .onion hosts, so a BIP 21 pj= value of http://x.onion:1@evil.com/pj passed as an onion endpoint, bypassing the check, and a v1 sender posted the Original PSBT in cleartext to evil.com. The https form redirects a v1 sender to an attacker host as well. The v2 sender is not affected because the encapsulated request authority is rebuilt from host_str() and the relay URL is sender-configured.

Payjoin endpoints never carry credentials, so an @ anywhere in the authority is now rejected with a new ParseError::UserinfoNotSupported (the enum is non_exhaustive, so this is additive & non-breaking). Port parse errors now propagate, which makes InvalidPort reachable and rejects inputs such as https://host:abc/ that were previously accepted with the junk stored in the path. https://host:/ still parses as no port, and @ remains valid in the path, query, and fragment.

Tests cover the host-confusion inputs, userinfo rejection with and without a port and for IPv6 literals, @ outside the authority, malformed ports, the .onion userinfo form through Uri::try_from and PjParam::parse, and the extended into_url_rejects_userinfo cases. The Arbitrary impl for Url builds the struct directly and the fuzz targets never reparse the mutated value, so they need no change.

Affects every release that ships the in-tree URL parser, 1.0.0-rc.3 through 1.0.0.

This was caught by loupe. It isn't really breaking anything about the threat model but does make the v1 reference actually spec compliant. For that reason, I'd like to get it through. Tagging @benalleng becaus he authored most of the Url module.

Disclosure: co-authored by Claude Code

@coveralls

coveralls commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 35750962934

Coverage increased (+0.04%) to 86.739%

Details

  • Coverage increased (+0.04%) from the base build.
  • Patch coverage: 1 uncovered change across 1 file (62 of 63 lines covered, 98.41%).
  • 2 coverage regressions across 1 file.

Uncovered Changes

File Changed Covered %
payjoin/src/core/url.rs 50 49 98.0%
Total (3 files) 63 62 98.41%

Coverage Regressions

2 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
payjoin/src/core/url.rs 2 95.12%

Coverage Stats

Coverage Status
Relevant Lines: 16764
Covered Lines: 14541
Line Coverage: 86.74%
Coverage Strength: 337.86 hits per line

💛 - Coveralls

Comment thread payjoin/src/core/into_url.rs Outdated
"https://user:pw@example.com:8080/",
] {
let err = input.into_url().unwrap_err();
assert_eq!(err.to_string(), "userinfo not supported", "{input}");

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

making one push to make use of PartialEq here instead

@DanGould
DanGould force-pushed the url-userinfo-host-confusion branch from b789e46 to 0263d2d Compare September 22, 2026 12:48
@DanGould
DanGould marked this pull request as ready for review September 22, 2026 12:48
Url::parse had no notion of RFC 3986 userinfo. parse_host ended the
host at the first ':' and any error from parse_port was discarded, so
for https://trusted.com:1234@evil.com/pj the parsed host was
trusted.com while the tail ":1234@evil.com" was stored as the path and
re-serialised verbatim. domain() and host_str() then disagreed with
as_str(): a standards-compliant client handed as_str() reads that
authority as userinfo "trusted.com:1234" and host "evil.com".

The v1 endpoint check consults domain() to allow plain http for .onion
hosts, so a BIP 21 pj= value of http://x.onion:1@evil.com/pj passed as
an onion endpoint and the Original PSBT was posted in cleartext to
evil.com. The https form redirects a v1 sender to an attacker host too,
since its certificate is valid for evil.com. The v2 sender is not
affected: the encapsulated request authority is rebuilt from host_str()
and the relay URL is sender-configured.

Payjoin endpoints never carry credentials, so an '@' anywhere in the
authority is now rejected with the new ParseError::UserinfoNotSupported
rather than parsed and forwarded. ParseError is non_exhaustive, so the
new variant is not a breaking change. Port errors now propagate, which
makes InvalidPort reachable and rejects inputs such as
https://host:abc/ that were previously accepted with the junk stored in
the path. An empty port (https://host:/) still parses as "no port".
'@' remains valid in the path, query, and fragment.

The Arbitrary impl for Url constructs the struct directly and the
fuzz targets never reparse the mutated value, so it needs no change.
@benalleng

Copy link
Copy Markdown
Collaborator

CACK seems simple enough, I remember in the past there was some discussion early on about how much we want to limit our URL parse to accept some things like this for directories, but this seems like the right approach to be opinionated here.

Waiting for some test cleanup on a merge ACK

@DanGould
DanGould force-pushed the url-userinfo-host-confusion branch from 0263d2d to 4606377 Compare September 22, 2026 15:58

@benalleng benalleng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

TACK 4606377

Fuzzed for ~2 cpu/hrs @ 100000 exec/s on url_decode_url

@DanGould
DanGould merged commit 25d7269 into payjoin:master Sep 22, 2026
13 checks passed
@DanGould
DanGould deleted the url-userinfo-host-confusion branch September 22, 2026 17:28
@DanGould DanGould mentioned this pull request Sep 23, 2026
14 tasks
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.

3 participants