Reject userinfo and malformed ports in Url::parse - #1896
Merged
Merged
Conversation
Collaborator
Coverage Report for CI Build 35750962934Coverage increased (+0.04%) to 86.739%Details
Uncovered Changes
Coverage Regressions2 previously-covered lines in 1 file lost coverage.
Coverage Stats
💛 - Coveralls |
DanGould
commented
Sep 22, 2026
| "https://user:pw@example.com:8080/", | ||
| ] { | ||
| let err = input.into_url().unwrap_err(); | ||
| assert_eq!(err.to_string(), "userinfo not supported", "{input}"); |
Member
Author
There was a problem hiding this comment.
making one push to make use of PartialEq here instead
DanGould
force-pushed
the
url-userinfo-host-confusion
branch
from
September 22, 2026 12:48
b789e46 to
0263d2d
Compare
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.
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
force-pushed
the
url-userinfo-host-confusion
branch
from
September 22, 2026 15:58
0263d2d to
4606377
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Url::parsehad no notion of RFC 3986 userinfo. The host ended at the first:and any error from the port parser was discarded, sohttps://trusted.com:1234@evil.com/pjparsed with hosttrusted.comand the tail:1234@evil.comstored as the path and re-serialised verbatim.domain()andhost_str()then disagreed withas_str(). A standards-compliant client such as reqwest reads that authority as userinfotrusted.com:1234and hostevil.com.The v1 endpoint check consults
domain()to allow plain http for.onionhosts, so a BIP 21pj=value ofhttp://x.onion:1@evil.com/pjpassed as an onion endpoint, bypassing the check, and a v1 sender posted the Original PSBT in cleartext toevil.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 fromhost_str()and the relay URL is sender-configured.Payjoin endpoints never carry credentials, so an
@anywhere in the authority is now rejected with a newParseError::UserinfoNotSupported(the enum isnon_exhaustive, so this is additive & non-breaking). Port parse errors now propagate, which makesInvalidPortreachable and rejects inputs such ashttps://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.onionuserinfo form throughUri::try_fromandPjParam::parse, and the extendedinto_url_rejects_userinfocases. TheArbitraryimpl forUrlbuilds 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