Skip to content

[Audit] [MEDIUM] Project handles can return verified handles with unblocked invisible Unicode controls #155

Description

@mejango

Audit seed

Fresh Nemesis / nana-project-handles-v6 all src/**/*.sol and script/**/*.sol / identity verification + input monoculture breaker / Codex

Repos involved

  • nana-project-handles-v6

Root cause

JBProjectHandles.setEnsNamePartsFor rejects empty labels, dots, ASCII controls, DEL, exact eth, and a selected set of Unicode format controls. The blocklist omits other invisible format controls such as U+2060 WORD JOINER. Accepted bytes are stored in _ensNamePartsOf and later returned verbatim by handleOf as a verified handle if the ENS juicebox text record matches.

Relevant code:

  • src/JBProjectHandles.sol:115-118 validates each byte and calls _isDisallowedUnicodeFormat.
  • src/JBProjectHandles.sol:228-255 only blocks selected Unicode format-control sequences.
  • src/JBProjectHandles.sol:124 stores accepted parts.
  • src/JBProjectHandles.sol:198 returns _formatHandle(ensNameParts) after ENS text-record verification.

Impact

A setter can publish a visually deceptive verified handle containing invisible formatting bytes. This does not create direct fund loss, but it breaks the identity registry's display-safety invariant and can mislead frontends, indexers, and users that treat handleOf output as safe verified metadata.

Proof of concept

Added local audit PoC: test/audit/CodexNemesisUnicodeFormatBypass.t.sol.

Sequence:

  1. Store a label containing U+2060: unicode"safe\u2060evil".
  2. Mock the ENS registry to return a resolver for the exact node.
  3. Mock the resolver text record to return "1:123".
  4. Call handleOf(1, 123, setter).
  5. The call returns the stored handle and its byte length is bytes("safeevil").length + 3, confirming the invisible UTF-8 sequence survived verification.

Verification command:

forge test --match-path test/audit/CodexNemesisUnicodeFormatBypass.t.sol -vvv

Result:

[PASS] test_verifiedHandleCanContainWordJoinerFormatControl()

Full suite result after adding the PoC:

71 tests passed, 0 failed, 0 skipped

Why this survived self-review

The strongest counter-argument is that callers are instructed to submit ENS-normalized labels offchain. That does not eliminate the issue because the contract explicitly attempts to reject dangerous formatting controls before storage, then treats the stored bytes as display-safe verified output. Once accepted, no later normalization or display-safety check occurs before handleOf returns the value.

Recommended fix

At minimum, reject the full U+2060-U+206F invisible/format-control range:

if (second == 0x81) return third >= 0xa0 && third <= 0xaf;

Consider also blocking other common invisible or display-altering codepoints such as U+00AD, U+034F, U+180E, U+FE00-U+FE0F, and variation selector supplement codepoints if non-ASCII labels remain supported. A stricter alternative is an onchain allowlist for display labels plus ENSIP-15 normalization offchain.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions