90593 w a insight the token info page publishes the namehash of a name s punycode a label so every non ascii ens name shows the node of a different separately registerable name
Submitted on Aug 25th 2026 at 20:04:51 UTC by @rev3rse for Audit Competition | ENS
- Report ID: #90593
- Report Type: Websites & Apps
- Report severity: Insight
- Target: https://github.com/immunefi-team/audit-comp-ens/tree/audit-comp-ready/apps/portal
- Impacts:
- Causing the application to display or pre-fill an incorrect address, name, or transaction detail, so that a user sends funds or submits a transaction to an unintended recipient, such as:
- resolving or rendering an ENS name to an attacker-controlled address
- showing the wrong recipient or amount on a transaction the user then approves
- Causing the application to display or pre-fill an incorrect address, name, or transaction detail, so that a user sends funds or submits a transaction to an unintended recipient, such as:
Description
Brief/Intro
routes/$name/token.tsx:288 computes namehash(asciiEncode(name)), and asciiEncode (utils/token/ascii.ts) is new URL('https://' + name).hostname — the WHATWG/UTS-46 host parser. ENS namehash is defined over the UTF-8, ENSIP-15-normalized name, so for every non-ASCII name the page prints the node of a different name, beneath a green "Normalized" badge and next to a copy button.
No attacker and no malformed input are required: münchen.eth is fully normalized, isNormalized() returns true, and the badge is green above a wrong node. Of the 16 production namehash( call sites under apps/, this is the only one that hashes an IDNA-transformed value.
Vulnerability Details
// routes/$name/token.tsx
211 namehash(name) // ENSv1 token id — correct
288 const hash = namehash(ascii) // the Namehash row — the defect
Two rows derived from the same route parameter, 77 lines apart in one file, returning different values. utils/token/ascii.ts returns xn--mnchen-3ya.eth for münchen.eth, and ens_normalize throws on that string (invalid label extension: "xn--") — the value has no intended consumer.
Impact Details
The Explorer publishes, under the victim's name, an identifier that resolves to a separately registerable node. The Manager accepts xn-- labels (- is absent from INVALID_LABEL_CHARS), so an attacker registers the twin and owns the node the Explorer prints. On the fork, registry.ownerOf(0x6525ac23…) returns the attacker; the victim's own name and an unregistered id are the negative controls.
The app's single ENSIP-15 gate on that path fires after the mint (setPrimaryName.ts:49), so the attacker sees "Registration Failed" for a name that is already theirs.
Recommended Fix
namehash(name)alone is not the fix —asciiEncodelower-cases (correct) and punycodes (incorrect), so raw is wrong forALICE.ethwhere the current code is right:
raw asciiEncode normalize
ALICE.eth 0x7ccc7bd6 ✗ 0x787192fc ✓ 0x787192fc
münchen.eth 0xbbb99291 ✓ 0x6525ac23 ✗ 0xbbb99291
Use namehash(normalize(name)) — correct in both directions, and the pattern useNameHistoryTimeline.ts:234 already uses.
References
apps/portal/src/routes/$name/token.tsx(288, 211) — the defect and the same-file contradiction 77 lines apart.apps/portal/src/utils/token/ascii.ts(1-7) —asciiEncodeisnew URL('https://'+name).hostname, i.e. IDNA ToASCII.apps/portal/src/routes/$name/resolver.tsx(170, 198) — a row also labelledNamehash, computed correctly from the same parameter.apps/portal/src/utils/token/isNormalized.ts(18) —ens_normalize(label) === label, the green badge's source; true formünchen.eth.apps/manager/src/features/register-v2/utils/name-parser.ts(11) —INVALID_LABEL_CHARSomits-, which is why thexn--twin registers.
Link to Proof of Concept
https://gist.github.com/almog-shachar/c2a3e14a55963f5a8a6f682ee0d02b6c
Proof of Concept
Single-file patch, 1 file changed. 18 of 18 pass offline (case 10, the chain half, is skipped and prints a banner saying a skip is not a pass).
git checkout 1c9b47f1 && git apply poc.patch && pnpm install
cd apps/portal && ./node_modules/.bin/vitest run src/__ens_poc_token_namehash__/
The rendering arm mounts the app's real Route through a memory router — attached with the same TokenRoute.update(...) call the generated routeTree.gen.ts makes — and reads the assertions out of the mounted DOM, not out of a re-implementation. R7 proves fixture-independence: replace the owner fixture entirely and the Namehash row does not move. R5 is the sharpest: münchen.eth (green Normalized) and xn--mnchen-3ya.eth (red Not Normalized) render the byte-identical node.
evidence/SUBMISSION-EXHIBIT.png — one image, two panels, Sepolia fork pinned at block 11562283:
/münchen.eth/token— green Normalized,ASCII xn--mnchen-3ya.eth,Namehash 0x6525ac23…. TheDNS encodedrow two lines above is0x086dc3bc…— UTF-8münchen— so the card uses the name where punycode is mandatory and the punycode form where it is forbidden./münchen.eth/resolver— a row also labelledNamehash, same app, same parameter:0xbbb99291…, the correct node.
Adding gist link here again: https://gist.github.com/almog-shachar/c2a3e14a55963f5a8a6f682ee0d02b6c