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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions