fix: reject punycode hosts with non-public ASCII TLDs - #32
Merged
Merged
Conversation
Kikobeats
marked this pull request as ready for review
August 5, 2026 09:21
Punycode matching disabled url-regex-safe's TLD list so IDN labels could accept .local/.internal/arbitrary suffixes that ASCII hosts reject. Derive the TLD list from the final hostname label instead, which also drops the punycode-regex dependency. Co-authored-by: kikohumanbeatbox <kikohumanbeatbox@gmail.com>
Kikobeats
force-pushed
the
cursor/critical-bug-management-9f33
branch
from
August 5, 2026 15:33
7c5bc61 to
802d669
Compare
Kikobeats
added a commit
that referenced
this pull request
Aug 5, 2026
`new URL()` already applies IDNA, IPv4/IPv6 validation and percent-encoding, so the only question left is whether the host is one the public internet can resolve. Answering that directly replaces the round-trip through url-regex-safe, a matcher built to find URLs inside prose. Closes the fake-IDN-TLD hole: url-regex-safe's list holds no `xn--` entries, so #32 had to inject the host's own TLD, which is a check that passes by construction. `https://xn--80a0aaa.xn--totallyfaketld/` was accepted and the design could not reject it. Mapping the list to punycode once at load makes the punycode case fall out instead of needing a special case, so the IPv6 exception, the TLD injection and the second origin-only pass all go away along with the url-regex-safe and re2 dependencies. Fuzz-differentialled against the previous implementation over 300k inputs. Every divergence falls in one of four classes: - paths ending in prose punctuation (`/a.`, `/?q=1.`, `/#x!`) are now accepted; url-regex-safe forbids a trailing `. ? !` because in prose it is sentence punctuation - ports below 10 (`http://example.com:1/`) are now accepted - an underscore is now allowed in any interior label, not only in a bare second-level one (`x.a_b.com` was rejected while `a_b.com` was accepted) - a host whose TLD is not in the public suffix list is now rejected in punycode form too Per call: 21.6us to 0.29us on ordinary hosts, 3.8us to 0.34us on punycode. Claude-Session: https://claude.ai/code/session_01G3AcnZdy222rWCkptbywJT Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Bug and impact
When the hostname contains a punycode (
xn--) label,url-httpdisablesurl-regex-safe's public TLD list (tlds: []). That lets IDN hosts with non-public ASCII suffixes pass validation while the equivalent ASCII hosts are rejected.Concrete triggers that previously returned the href instead of
false:https://example.local/→false(correct)https://xn--e1afmkfd.local/→ accepted (incorrect)https://пример.local/→ accepted (incorrect; normalizes to the punycode form above)https://xn--80a0aaa.internal/→ accepted (incorrect)https://xn--80a0aaa.invalidtld/→ accepted (incorrect)Callers that treat a truthy result as a validated public HTTP(S) URL can accept private/special-use or garbage suffixes via an IDN label — an SSRF-adjacent validation bypass.
This is distinct from #30 (credentialed-URL bypass) and #31 (path-substring authority bypass).
Root cause
Punycode matching uses
tlds: []so IDN TLDs likexn--p1aistill match (the default list has noxn--entries). An empty TLD alternation effectively skips public-suffix validation for any host that merely contains a punycode label.Fix
After a successful punycode match, require the hostname's final label to either:
xn--*), orurl-regex-safecheck with the default public TLD list (viaorigin + '/')Valid cases such as
https://xn--80a0aaa.com/andhttps://example.xn--p1ai/still accept.Validation
.local/.internal/ arbitrary suffix punycode hostsnpm testpasses (lint + Ava, 100% coverage)Note
Medium Risk
Changes URL validation behavior for IDN/punycode hosts (SSRF-adjacent); intended to tighten acceptance but could affect edge-case URLs that previously passed.
Overview
Fixes a validation bypass where any hostname containing an
xn--label usedtlds: [], so url-regex-safe skipped public-suffix checks and accepted IDN hosts on.local,.internal, or fake suffixes while the ASCII equivalents were rejected.TLD handling now derives the last label only: custom
tldsis passed when that label is an IDN TLD (xn--*), otherwise the default public TLD list applies. Punycode in non-TLD labels no longer disables suffix validation. The punycode-regex dependency is removed; IPv6 still uses non-exact matching only.Regression tests cover punycode/Unicode hosts on private and invalid suffixes.
Reviewed by Cursor Bugbot for commit 802d669. Bugbot is set up for automated code reviews on this repo. Configure here.