Repository navigation
Conversation
Under a wildcard entry of the public suffix list (*.mm, *.kh, *.nom.br, *.sch.uk), every name directly below the base is itself a public suffix. Such a name failed PublicSuffix.valid?, fell through to a Domain with no second- or third-level label, and no rule ran on it, so раураӏ.mm and friends went undetected. Below one, the wildcard label was folded into the TLD, so it was never checked either, and a Cyrillic wildcard label made Cyrillic look-alikes next to it acceptable. Split the domain at the part of the suffix the registry spells out, check the wildcard label like any other, and keep PublicSuffix's own sld/trd split for the rest. Ignore private entries consistently, so names matching one (раураӏ.compute.amazonaws.com) are checked too. The Myanmar allow-list now matches the .mm registry suffix directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…abel Joining the subdomain chain into one string made the mixed-scripts rule flag a Cyrillic label next to an ASCII one (пример.compute.amazonaws.com), which Chrome allows: it checks scripts per label. Names matching a private wildcard entry newly reached that, so split every label out. The sanitizer substitutes the original-cased label, but only the first casing was recovered, so РАУРАӀ.раураӏ.mm left раураӏ unencoded. Return a detection for every distinct casing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Searching the domain for every casing of a detected label matched it inside longer safe labels too (раураӏ.РАУРАӀмузей.mm), and rebuilding the casing map for every rule made repeated labels quadratic. Pair each checked label with the label as written by position instead, and check a label repeated verbatim once. Take the labels by stripping the registry suffix off the name rather than through PublicSuffix's line-anchored match, which skipped anything before a newline. The sanitizer replaced every occurrence of a detected label, including inside longer labels (а inside академия). Replace whole occurrences only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A display name in another encoding (UTF-16) is reported as invalid_unicode, and interpolating it into the UTF-8 pattern raised. Substitute it wherever it appears, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A label followed by punctuation or a format character inside a longer label (ד in ד׳שלום, а in а) still counted as a whole occurrence. Require a separator, the edge of the field, a dot, the @, or the quotes and brackets around an address, on both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No list of separators identifies a whole label in every field the sanitizer handles: escaped display names and malformed labels with spaces or punctuation still left a detected label partly encoded. Go back to substituting every occurrence, as before, and do the longest label first, so encoding a short detected label inside a longer one can't leave the longer one unencoded now that each label is checked on its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gsub reads backslash sequences in a replacement string, so a label with \` inserted the text before the match; with every label substituted on its own, twenty such labels grew a 131-byte domain to 4 MB. Substitute through a block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approved
The implementation is internally consistent and includes focused regression coverage for the identified edge cases.
0 open findings
What changed in this PR
Detects spoofing across individual labels, including wildcard and private public suffix cases, while hardening sanitization.
Changes:
- Splits domains by registry-controlled suffix and checks each label independently.
- Corrects Myanmar suffix handling.
- Safely sanitizes nested labels, backslashes, casing, and non-UTF-8 input.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or rungh pr ready --undo.
Click "Ready for review" or rungh pr readyto reengage.
| File | Description |
|---|---|
lib/homographic_spoofing/detector/idn.rb |
Implements per-label detection and registry suffix splitting. |
lib/homographic_spoofing/detector/rule/idn/script_confusable.rb |
Updates Myanmar suffix matching. |
lib/homographic_spoofing/sanitizer/base.rb |
Safely orders and replaces detected labels. |
test/detector/idn_test.rb |
Covers wildcard, private suffix, exception, and per-label behavior. |
test/sanitizer/idn_test.rb |
Covers wildcard and replacement edge cases. |
test/sanitizer/quoted_string_test.rb |
Covers non-UTF-8 sanitization. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This was referenced Oct 10, 2026
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.
The public suffix list has wildcard entries such as
*.mm,*.kh,*.nom.brand*.sch.uk: every name directly under the base is itself a public suffix. Lookalike labels in those positions went undetected:раураӏ.mm,раураӏ.kh,раураӏ.nom.br,раураӏ.sch.ukfailedPublicSuffix.valid?, fell through to aPublicSuffix::Domainwith no second- or third-level label, and no rule ran on them at all.login.раураӏ.mm), the wildcard label was folded into the TLD, so it was never checked. A Cyrillic wildcard label also made Cyrillic lookalikes next to it acceptable (ѕсоре.почта.kh), because the TLD then "contained" Cyrillic.раураӏ.compute.amazonaws.com), becausevalid?considered private entries andparsedidn't.What changes
Detector (
Detector::Idn)mmfor*.mm;kawasaki.jpfor!city.kawasaki.jp), and checks the wildcard label like any other label. Private entries are ignored throughout, so names under them are ordinary names.пример.compute.amazonaws.com) tripped the mixed-scripts rule; per label it doesn't.РАУРАӀ.раураӏ.mm) without matching inside longer labels, and stays linear on many repeated labels..mmregistry suffix directly (/mm/instead of/[a-z]+\.mm/), since the wildcard label is no longer part of the TLD.Sanitizer (
Sanitizer::Base)аinsideа) can't leave the longer one unencoded.\`grew a 131-byte domain to 4 MB).Behaviour changes worth knowing
ทนบพรห.mm) are flagged too, as they would be under any non-Thai TLD.раураӏ.www.ac.ru,ทนบพรห.www.shop.th). Each label alone was already safe there..раураӏ.mm) are nowinvalid_domain, as they are elsewhere.а.академия.com). Main already does this, and it never leaves a lookalike unencoded.Overlap with #76 and #77
#76 also moves every rule to one label at a time, and #77 (stacked on it) scopes sanitizer replacements to the address part each detection came from. This PR's per-label split and sanitizer changes overlap with both and will conflict. If #76 lands first, this PR shrinks to the wildcard and private-entry split plus the sanitizer ordering and literal-replacement fixes, rebased onto it. If this lands first, #76 rebases onto it.
Verification
bin/test: 58 runs, 398 assertions, 0 failures, with new tests for the four wildcard bases, exception entries, private entries, per-label checks and the sanitizer cases above.main. On the single-label set, the only new results are detections under wildcard bases and private entries. Nothing that was detected stopped being detected. The chain set shows the cross-label losses described above. These are synthetic names, not real traffic.mainfor any TLD and is out of scope here.🤖 Generated with Claude Code