Skip to content

Reject hosts with Unicode default-ignorable code points - #1801

Merged
bdraco merged 1 commit into
masterfrom
fix-encode-host-default-ignorable
Jul 19, 2026
Merged

bdraco merged 1 commit into
masterfrom
fix-encode-host-default-ignorable

Conversation

@bdraco

@bdraco bdraco commented Jul 19, 2026 •

Copy link
Copy Markdown
Member

What do these changes do?

Invisible default-ignorable and format code points (soft hyphen, zero-width space, word joiner, bidi controls, variation selectors, and so on) let the parsed host diverge from the string the caller validated. Depending on the code point, IDNA either silently deletes it (so e<ZWSP>vil.com encoded to evil.com) or folds it into a different punycode host; in both cases .host and str(url) no longer matched the input. _encode_host now rejects any host containing such a code point before IDNA encoding. This runs on every path, including the plain URL(str) constructor and the builder APIs, so the parsed host cannot diverge from the input. This matches idna, httpx and urllib3, which all reject these hosts.

The rejected set is the union of two authoritative sources, matching the two encoders _idna_encode dispatches to: the Unicode Default_Ignorable_Code_Point property (for the idna uts46 path) and RFC 3454 Stringprep Table B.1 "commonly mapped to nothing" (for the stdlib str.encode("idna") fallback). The latter is the source of U+1806, which is not default-ignorable but is still stripped by the fallback.

Are there changes in behavior for the user?

Yes; a host containing a default-ignorable code point is now rejected with ValueError instead of being silently collapsed to a different host.

Is it a substantial burden for the maintainers to support this?

No; the check is a single regex search on the already-slow non-ascii branch of _encode_host, and the ascii hot path is untouched. A sweep test brute-forces every code point through _idna_encode and fails if any point it silently deletes is not covered by the character class, so the hand-written ranges stay pinned to the installed idna/Unicode data rather than drifting.

Related issue number

Reported by @bilguunbicktivism; thanks for the report.

Checklist

  • I think the code is well written
  • Unit tests for the changes exist
  • Documentation reflects the changes N/A (behavior fix, no public API surface change)
  • If you provide code modification, please add yourself to CONTRIBUTORS.txt
  • Add a new news fragment into the CHANGES/ folder

@pyba-chronographer pyba-chronographer Bot added the bot:chronographer:provided There is a change note present in this PR label Jul 19, 2026
Comment thread yarl/_url.py Fixed
Comment thread yarl/_url.py Fixed
@codspeed

codspeed Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 97 untouched benchmarks


Comparing fix-encode-host-default-ignorable (211abb6) with master (3921a73)

Open in CodSpeed

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 29702735471

Warning

No base build found for commit 277b9b2 on master.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 98.75%

Details

  • Patch coverage: 34 of 34 lines across 4 files are fully covered (100%).

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 6261
Covered Lines: 6182
Line Coverage: 98.74%
Relevant Branches: 61
Covered Branches: 61
Branch Coverage: 100.0%
Branches in Coverage %: Yes
Coverage Strength: 0.99 hits per line

💛 - Coveralls

@aiolibsbot

aiolibsbot commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@codecov

codecov Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.86%. Comparing base (277b9b2) to head (211abb6).
⚠️ Report is 4 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff            @@
##           master    #1801    +/-   ##
========================================
  Coverage   99.86%   99.86%            
========================================
  Files          21       21            
  Lines        4328     4462   +134     
  Branches      252      254     +2     
========================================
+ Hits         4322     4456   +134     
+ Misses          4        3     -1     
- Partials        2        3     +1     
Flag Coverage Δ
CI-GHA 99.79% <100.00%> (+<0.01%) ⬆️
OS-Linux 99.79% <100.00%> (+<0.01%) ⬆️
OS-Windows 98.63% <100.00%> (+0.11%) ⬆️
OS-macOS 98.76% <100.00%> (+0.03%) ⬆️
Py-3.10 99.75% <100.00%> (+0.05%) ⬆️
Py-3.11 99.75% <100.00%> (+0.05%) ⬆️
Py-3.12 99.75% <100.00%> (+0.05%) ⬆️
Py-3.13 99.77% <100.00%> (+0.03%) ⬆️
Py-3.14 99.77% <100.00%> (+0.03%) ⬆️
Py-3.14t 99.77% <100.00%> (+0.03%) ⬆️
Py-pypy-3.11 99.43% <100.00%> (+0.06%) ⬆️
VM-macos-latest 98.76% <100.00%> (+0.03%) ⬆️
VM-ubuntu-latest 99.79% <100.00%> (+<0.01%) ⬆️
VM-windows-latest 98.63% <100.00%> (+0.11%) ⬆️
pytest 99.79% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bdraco
bdraco force-pushed the fix-encode-host-default-ignorable branch from 259e84e to f5b9eac Compare July 19, 2026 20:54
@bdraco
bdraco requested a review from Copilot July 19, 2026 20:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens yarl host handling by rejecting Unicode Default_Ignorable_Code_Point characters before IDNA/UTS-46 processing, preventing hosts from being silently normalized into a different value than the caller supplied.

Changes:

  • Added a pre-IDNA validation step in _encode_host() to reject hosts containing default-ignorable Unicode code points.
  • Added pytest coverage for the URL(...) constructor and builder APIs (URL.build(), URL.with_host()).
  • Updated docs spelling wordlist and added a towncrier news fragment for the behavior change.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
yarl/_url.py Adds _DEFAULT_IGNORABLE_RE and rejects default-ignorables before IDNA encoding in _encode_host().
tests/test_url.py Adds constructor-level test coverage for rejecting default-ignorable code points in hosts.
tests/test_url_update_netloc.py Adds with_host() test coverage for rejecting default-ignorable code points.
tests/test_url_build.py Adds URL.build() test coverage for rejecting default-ignorable code points.
docs/spelling_wordlist.txt Adds ignorable to satisfy doc spelling.
CHANGES/1801.bugfix.rst Adds release note fragment describing the behavior change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread CHANGES/1801.bugfix.rst Resolved
@coveralls

coveralls commented Jul 19, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 29704148905

Coverage increased (+0.01%) to 98.779%

Details

  • Coverage increased (+0.01%) from the base build.
  • Patch coverage: 68 of 68 lines across 4 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 6408
Covered Lines: 6329
Line Coverage: 98.77%
Relevant Branches: 61
Covered Branches: 61
Branch Coverage: 100.0%
Branches in Coverage %: Yes
Coverage Strength: 0.99 hits per line

💛 - Coveralls

@bdraco
bdraco force-pushed the fix-encode-host-default-ignorable branch from f5b9eac to 8f20056 Compare July 19, 2026 21:09
Comment thread yarl/_url.py Dismissed
Comment thread yarl/_url.py Dismissed
@aiolibsbot

aiolibsbot commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

IDNA/UTS-46 mapping silently deletes default-ignorable code points
(soft hyphen, zero-width space, word joiner, bidi controls, variation
selectors, and so on), so a host like 'e<ZWSP>vil.com' encoded to
'evil.com', a different host than the input string. _encode_host now
rejects any host containing such a code point before IDNA encoding, on
every path including the plain URL(str) constructor (validate_host=False)
and the builder APIs, so the parsed host cannot diverge from the input.
This matches idna, httpx, and urllib3.
@bdraco
bdraco force-pushed the fix-encode-host-default-ignorable branch from 8f20056 to 211abb6 Compare July 19, 2026 21:19
@bdraco
bdraco marked this pull request as ready for review July 19, 2026 21:19
@aiolibsbot

Copy link
Copy Markdown
Contributor

PR Review — Reject hosts with Unicode default-ignorable code points

Merge-ready. Solid, well-pinned defense against invisible-host confusion; only a non-blocking note on test runtime.

  • Check placement verified: it sits after the ascii early-return and the IP block, so every non-ascii host (and all default-ignorables are >= U+00A0) hits it, while the ascii hot path is genuinely untouched.
  • Prior review's U+1806 gap is closed in both the regex and the parametrized tests; the from None and error-message shape match the existing NOT_REG_NAME raises.
  • The sweep test correctly pins the hand-written character class to installed idna/Unicode data; ranges (highest U+E0FFF) line up with the swept blocks, surrogates are skipped, and empty intermediate planes are accounted for.
  • CHANGES wording already widened to "silently drops or otherwise folds into a different host", addressing the prior suggestion; CodeQL range alerts are confirmed false positives (\ufff0-\ufff8 does not include \ufffd).
  • One suggestion: the sweep runs ~135k uncached IDNA encodes; fine functionally, worth watching for CI slowness.

🟢 Suggestions

1. Sweep test brute-forces ~135k code points through IDNA
tests/test_url.py:2694-2712

test_default_ignorable_covers_idna_stripped iterates range(0x00A0, 0x20000) plus range(0xE0000, 0xE1000) (~135k code points), calling _idna_encode on a distinct string for each. Because every input is unique, the _idna_encode LRU gives no benefit, so this is ~135k real uts46 encodings.

Why it matters: this is a genuinely valuable pin against idna/Unicode data drift (the design here is good), but it can take tens of seconds and runs on every suite invocation. If it shows up as a slow test in CI wall-clock, consider narrowing the sweep to the blocks that actually hold Default_Ignorable code points, or gating it behind a marker.

Not blocking; the coverage run confirms it completes, and correctness is not in question.

for lo, hi in ((0x00A0, 0x20000), (0xE0000, 0xE1000))
    for cp in range(lo, hi)

Checklist

  • Check placement covers all non-ascii host paths
  • Ascii hot path unaffected
  • Reject set pinned to idna/Unicode data via sweep test
  • Prior finding (U+1806) resolved
  • New behavior branches tested and covered
  • CHANGES fragment, spelling wordlist, PR body follow conventions
  • Static-analysis alerts triaged as false positives
  • Test runtime reasonable — suggestion #1

Automated review by Kōan (Claude) HEAD=211abb6 1 min 39s

@bdraco
bdraco merged commit 51e2802 into master Jul 19, 2026
57 checks passed
@bdraco
bdraco deleted the fix-encode-host-default-ignorable branch July 19, 2026 21:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided There is a change note present in this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants