[pre-flight] Confine non-special-scheme 'self' matching to a registered scheme allowlist - #3
Draft
jonathanKingston wants to merge 4 commits into
Draft
[pre-flight] Confine non-special-scheme 'self' matching to a registered scheme allowlist#3jonathanKingston wants to merge 4 commits into
jonathanKingston wants to merge 4 commits into
Conversation
added 4 commits
August 24, 2026 23:13
The 'self' fast path compares the protected resource's origin against url.origin(), but the URL standard gives every non-special-scheme URL an opaque origin - so for a document served over a scheme the user agent has registered itself, 'self' could never match even an exactly same-origin subresource, and the fallback branch only admits http(s)/ws(s). Any policy at all therefore blocked every same-origin subresource on such a document. A user agent that models those documents with tuple origins - the treatment Chromium and WebKit give schemes registered as "standard", and what makes `script-src 'self'` work on a chrome-extension:// or moz-extension:// page today - now gets component comparison: equal scheme, equal host, and equal ports, where "no port on either side" (tuple sentinel port 0, no default port for the scheme) also counts as equal. Same scheme only, and only for schemes with no default port. The existing relaxation is the http -> https/wss upgrade allowance, which is meaningful only for those schemes; extending it to a registered scheme would let 'self' in a custom://example.com document match https://example.com/. Special schemes are otherwise unaffected: a same-scheme match there was already taken by the origin-equality fast path.
…owlist The previous commit gave component comparison to every scheme without a default port, which changes what a policy means for schemes the caller never asked about. The specification says such a URL has an opaque origin and so matches nothing; departing from that should be something an embedder opts into, not the default. Chrome's version of this is an allowlist: the embedder registers its schemes as "standard" at start-up (url::AddStandardScheme, populated from ContentClient::AddAdditionalSchemes), and CSP host matching is gated on GURL::IsStandard - a URL whose scheme is not standard reports an empty host, so it matches no host-bearing source expression, 'self' included. Add `scheme_registry` as that list. It is empty by default, so a caller that does not opt in keeps exactly the behaviour the specification prescribes, and pays one relaxed atomic load to find that out. An embedder that registers a scheme is asserting that it, rather than the URL Standard, defines the origin of URLs with that scheme, and that it supplies the matching tuple origin. Registration grants same-scheme, same-host, same-port matching and nothing else, so 'self' on a registered custom://example.com document still does not reach https://example.com/. Registering a scheme the registry cannot speak for - a special scheme, or one with a default port a tuple origin could not tell from an absent one - is refused rather than accepted and then ignored at matching time. Host-source expressions are untouched: they name their own scheme, so they never needed the registry and keep working without it.
The module doc explained the opaque-origin problem twice and carried a "Differences from Chromium" section longer than the code it described, and several tests restated their own names. Keep what a caller cannot infer - what registering a scheme grants, that the registry is empty by default, that the embedder must still supply the tuple origin - and drop the rest.
`default_port` already refuses ftp, http, https, ws and wss, so the list duplicated it for every entry but one. The exception was `file`, and excluding it bought little: a `file:///a/b` URL has no host, so component comparison cannot match it whatever the registry says. All the carve-out blocked was `file://server/path` against a matching tuple origin - which is the decision an embedder is making by registering the scheme at all. Leaves one condition, derived from the same `default_port` the matching site uses, so the two cannot drift.
jonathanKingston
force-pushed
the
self-tuple-origin
branch
from
August 27, 2026 11:26
4846d2e to
1d9a6bb
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.
Pre-flight CI run for the branch behind rust-ammonia#76. Not for merge here.
What changed
Review feedback on rust-ammonia#76 was that giving component comparison to every scheme without a default port "willfully violate[s] the spec by default", and that the client application should have to opt in. Chrome's version of that opt-in is an allowlist, so this follows Chrome's model.
The new commits add
scheme_registry, an allowlist of schemes whose origin the embedder — not the URL Standard — defines:The
'self'component-comparison branch added by the first commit is now gated on that registry. It is empty by default, so a caller that does not opt in keeps exactly the behaviour the specification prescribes: a non-special scheme has an opaque origin and'self'matches nothing.How Chrome does it
Chrome registers custom schemes as standard at start-up —
url::AddStandardScheme, populated fromContentClient::AddAdditionalSchemes— and gates CSP host matching onGURL::IsStandard:A URL whose scheme is not standard reports an empty host, so it matches no host-bearing source expression,
'self'included. Chrome's'self'source is itself built from the document URL's scheme, host and port (ComputeSelfOrigin), never from an origin, which is why an opaque origin does not stand in its way there.Deliberate departures from Chrome:
RwLock, so Chrome'surl::LockSchemeRegistriesstep (which exists because its vectors are unsynchronised) is not needed.SchemeTypeper scheme; every scheme here is treated asSCHEME_WITH_HOST_AND_PORT.is_standard_schemeisfalseforhttp,httpsand friends, whose origins the URL Standard already defines.Scope
'self'on a registeredcustom://example.comdocument still does not reachhttps://example.com/.'self'matching skips those schemes either way. Registration and the matching site read the samedefault_porttable, so they cannot drift.fileis registerable, since it has no default port. That admits onlyfile://server/pathagainst a matching tuple origin —file:///a/bparses with no host at all, so component comparison cannot match it however it is registered.'self'source expression.Testing
cargo test,cargo test --features=serde,cargo test --features=version-sync,cargo fmt --check, andcargo clippy --all-targets --all-features(no new warnings) all pass.tests/self-tuple-origin.rsgains coverage for the gate itself: an unregistered scheme matches nothing however exactly its components line up, registering one scheme admits no other, and host-source expressions still work without registration.