Skip to content

links: gate a url by its scheme, not its spelling - #11

Merged
LucideLarp merged 2 commits into
mainfrom
harden/url-gate-case
Jul 25, 2026
Merged

links: gate a url by its scheme, not its spelling#11
LucideLarp merged 2 commits into
mainfrom
harden/url-gate-case

Conversation

@LucideLarp

Copy link
Copy Markdown
Collaborator

HTTPS://example.com failed the url gate, so the link shipped as href="#": underlined, clickable and dead. Nothing in the repo is written that way today, which made it a trap rather than a live defect, but the identical shape had already cost a purchase route when a mailto: the gate did not allow degraded the same way.

Two changes, and a class settled.

The scheme is read case-insensitively. The two validators in build.js already were (c.url since before this branch, with a comment citing RFC 3986); only the gate that actually emits hrefs disagreed. The checkout validator and absUrl move with it, because a validator stricter than the emitter turns a working HTTPS:// checkout into "Checkout coming soon".

href="#" stops being the answer to a refused url. It degrades to its own words instead: visible, unclickable, honest. The build then names the file and the url, so a green build cannot hide it the way # did.

An http(s) url with no host is refused. Found by fuzzing rather than reading: https:// satisfies any scheme prefix, has no host and navigates nowhere, so it produced the same dead link through the accept path. Removing # alone did not close the class.

Where the failure lands differs on purpose: a url in store.config.json fails the build (one small hand-edited file, nine other fatal checks beside it, and a refused item url now drops a whole figure silently). A url in prose only warns, because this repo is forked and run by strangers on their own writing and refusing to deploy a store over one bad link in one paragraph costs them more than it saves.

Evidence

Differential against the gate at 30b4be6, 1,148,904 inputs in both link and src modes:

newly accepted 8,650, every one an uppercase http(s) url with a host
newly refused 48, every one a hostless http(s) url
returned values changed 0
accepted urls resolving off-origin 0

Resolution is checked with the WHATWG parser Node ships rather than by matching substrings of our own html, and that property is now in the suite: every accepted url must resolve to this origin, an explicit http(s) url, or a mailto. 197,904 hostile inputs (mixed case, tabs, newlines, NUL, vertical tab, protocol-relative in every spelling) are refused in both modes.

20 guards were mutation-tested; 17 go red. The three that do not are reported in the branch notes, one of which was a real gap in #-as-an-image-src and is now covered.

HTTPS:// failed the gate, so the link shipped as href="#": underlined,
clickable and dead. That is the shape that cost a mailto purchase route.
A refused url now degrades to its own words and the build names the file
and the url, instead of the page looking fine and going nowhere.

The checkout validator and absUrl move with the gate; a validator stricter
than the emitter turns a working HTTPS:// checkout into "Checkout coming
soon". Urls in store.config.json fail the build, prose only warns: one is
a small hand-edited file, the other is a stranger's writing.
…parser

Found by fuzzing the gate rather than reading it: "https://" satisfies the
scheme prefix, has no host, and shipped as an underlined link that goes
nowhere. Same dead link the "#" fallback made, arriving through the accept
path, so removing "#" alone did not close the class. store.config.json's
own "url" check has demanded a host since before the gate did.

The image sinks now emit the url safeUrl approved rather than the one they
were handed, so validate and emit cannot read different strings.

Tests pin the outcome against the WHATWG parser Node ships, not against
substrings of our own html: every accepted url must resolve to this origin,
an explicit http(s) url, or a mailto. A fragment is a link target and never
an image src, which nothing was holding.
@LucideLarp
LucideLarp merged commit 098954f into main Jul 25, 2026
20 of 21 checks passed
@LucideLarp
LucideLarp deleted the harden/url-gate-case branch July 25, 2026 10:05
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.

1 participant