92892 w a critical remove user revokes only ui recognized role bits and can leave official authority active
Submitted on Sep 13th 2026 at 04:22:06 UTC by @BugNetter for Audit Competition | ENS
- Report ID: #92892
- Report Type: Websites & Apps
- Report severity: Critical
- Target: https://github.com/immunefi-team/audit-comp-ens/tree/audit-comp-ready/apps/portal
- Impacts:
Description
Brief/Intro
The Portal converts authoritative resolver and registry role bitmaps into closed UI taxonomies, then builds Remove user revocations from only the represented roles. When an account holds a displayed role together with an official role that the UI does not represent, the removal transaction succeeds but the omitted role remains active. An administrator can therefore approve an apparently successful removal while the former delegate retains an official capability. This report is submitted as a Security Best Practices Insight and does not claim a Low-or-higher vulnerability.
The unlisted supporting-evidence Gist contains two source-bound Portal tests, preserved local-EVM receipts, a side-by-side root-cause trace, source provenance, and a SHA-256 ledger.
Insight Details
Shared root cause
Both removal paths reconstruct the revocation bitmap from a lossy UI projection rather than the account's authoritative raw bitmap. The resolver and registry demonstrations below use different contracts and roles, but they cross the same application boundary and have the same durable fix. They are two manifestations of one Insight, not two unrelated recommendations.
Resolver manifestation: omitted SET_DATA
The Portal's resolver role table omits the official SET_DATA role, and its decoder iterates only that table. The removal mutation accepts the decoded role list, and handleRemoveUser stores only that list. The revoke helper encodes the supplied roles, not the raw bitmap.
For an initial bitmap of SET_ADDR | SET_DATA (0x1000000001), the Portal displays SET_ADDR and prepares a revoke of only 0x1. The official contracts define SET_DATA = 1 << 36 and authorize setData with that role. The local official-bytecode replay leaves 0x1000000000 active, after which the former delegate changes generic resolver data from before-removal to after-removal.
The state-matched control revokes the complete raw bitmap, leaves roles 0x0, and rejects the identical later write with EACUnauthorizedAccountRoles(uint256,uint256,address) selector 0x4b27a133. The official access-control implementation exposes root grant, root revoke, and raw role lookup, while _revokeRoles clears only the supplied bits.
Registry manifestation: omitted CAN_NAME
The registry bitmap decoder iterates only the installed ENSJS registryRoles table, and useRegistryRoles converts the raw bitmap into that decoded list. CAN_NAME is not represented. The registry sheet derives removableRoles from the displayed roles, passes that list into the revoke step, and the helper encodes only those roles.
For an initial bitmap of SET_RESOLVER | CAN_NAME (0x1000000000000000000000001000000), the Portal prepares revokeRootRoles(0x1000000, delegate), leaving 0x1000000000000000000000000000000. The official registry defines CAN_NAME = 1 << 120 and implements isContractNamer as a root-role check. The official adapter routes setName through requireNamer, and AccountNamerLib consults isContractNamer or reverts UnauthorizedNamer.
The local replay reports isContractNamer(delegate) == true after the partial removal, and the former delegate changes the registry-managed reverse label from victim-managed.eth to former-delegate.eth. The state-matched complete-revoke control leaves roles 0x0, reports isContractNamer(delegate) == false, and rejects the same operation with UnauthorizedNamer(address) selector 0x0d1b7e4e.
Impact Details
This Insight is valuable wherever the Portal's role taxonomy is incomplete now or becomes incomplete after an official role is added. A successful Remove user action should mean that the selected account no longer has authority, but the current design can leave an unrepresented capability active without warning the administrator.
The demonstrated consequence is retained post-removal authority, not a pre-removal authorization bypass. The resolver mutation is generic IDataResolver.data storage and is not claimed to be a Portal-visible profile change. The registry storage boundary is mocked, although its authorization executes in pinned official adapter and UserRegistry bytecode. No production prevalence, affected-user count, funds loss, or Low-or-higher impact is claimed.
Recommendation
Use an invariant-preserving removal design:
- Read and revoke the authoritative raw bitmap for the selected account and resource; or
- Refuse removal and display a precise warning whenever the raw bitmap contains unknown bits.
Add regression coverage for both mixed bitmaps. Merely adding SET_DATA and CAN_NAME to today's tables is incomplete because a later official role could recreate the same defect.
References
- Unlisted supporting-evidence Gist
- Resolver role table and resolver bitmap decoder
- Resolver removal handler,
handleRemoveUser, and resolver revoke helper - Registry bitmap decoder,
useRegistryRoles, registry removal selection, registry revoke step, and registry revoke helper - Official ENS contract references:
SET_DATA,setData, root role operations,_revokeRoles,CAN_NAME,isContractNamer, reverse registrar adapter, andAccountNamerLib
Link to Proof of Concept
https://gist.github.com/Gonzo15918/22f3d2b5b33759ec1a7ea1806c6f3061
Proof of Concept
Proof of Concept
The complete supporting bundle is available in this unlisted Gist. It includes two source-bound Portal tests, preserved local-EVM receipts, a shared-root trace, official-source provenance, and a SHA-256 ledger.
Conditions under which this Insight is valuable
- A legitimate delegate holds a mixed bitmap containing one UI-represented role and one official role omitted from the UI taxonomy.
- The Portal decodes and displays only the represented role.
- An administrator selects Remove user and approves the displayed removal transaction.
- The Portal constructs the revoke bitmap from the decoded list rather than the authoritative raw bitmap.
- The transaction succeeds, but the omitted role remains active and can authorize a later operation by the former delegate.
This condition is security-relevant even without current production prevalence: a removal interface should either revoke the account's complete authority or explicitly warn that it cannot do so safely. Any present or future official role missing from the UI taxonomy can violate that invariant.
Step-by-step demonstration
A. Resolver manifestation
- Give the delegate the root bitmap
SET_ADDR | SET_DATA=0x1000000001. - Decode it through the Portal taxonomy. The displayed list contains
SET_ADDRbut omits officialSET_DATA. - Select Remove user. The Portal component test decodes the prepared call and observes a revoke bitmap of only
0x1. - Execute the equivalent partial revoke in the local official-bytecode replay. The call succeeds and leaves
0x1000000000(SET_DATA) active. - Call
setDataas the former delegate. It succeeds and changes the generic data value frombefore-removaltoafter-removal. - Repeat from matched initial state, but revoke the complete raw bitmap
0x1000000001. Roles become0x0; the same latersetDatacall is rejected with selector0x4b27a133, and the prior value is preserved.
B. Registry manifestation
- Give the delegate the root bitmap
SET_RESOLVER | CAN_NAME=0x1000000000000000000000001000000. - Decode it through the Portal/installed-ENSJS taxonomy. The displayed list contains
SET_RESOLVERbut omits officialCAN_NAME. - Select Remove user. The Portal component test decodes the prepared call and observes
revokeRootRoles(0x1000000, delegate). - Execute the equivalent partial revoke in the local replay. The call succeeds, leaves
0x1000000000000000000000000000000, andisContractNamer(delegate)remains true. - Call the official adapter's
setNamepath as the former delegate. Authorization succeeds and the mocked storage boundary records a change fromvictim-managed.ethtoformer-delegate.eth. - Repeat from matched initial state, but revoke the complete raw bitmap. Roles become
0x0,isContractNamer(delegate)becomes false, and the same later call is rejected with selector0x0d1b7e4e; the prior label is preserved.
The registry mock covers storage only. Authorization executes in the pinned official DefaultReverseRegistrarAdapter and UserRegistry bytecode.
Reproduce the Portal transaction-construction results
Use a clean checkout of https://github.com/immunefi-team/audit-comp-ens at commit 1c9b47f18fcddd2e864dfe385c4171061c9811ae, with its pinned dependencies already installed. Clone or download the Gist, then run:
export ENS_APP_REPO=/absolute/path/to/audit-comp-ens
export INSIGHT_GIST=/absolute/path/to/downloaded-gist
mkdir -p "$ENS_APP_REPO/apps/portal/.workflow-tests"
cp "$INSIGHT_GIST/INSIGHT-002_resolver_set_data.test.tsx" "$ENS_APP_REPO/apps/portal/.workflow-tests/"
cp "$INSIGHT_GIST/INSIGHT-002_registry_can_name.test.tsx" "$ENS_APP_REPO/apps/portal/.workflow-tests/"
corepack pnpm@10.27.0 --dir "$ENS_APP_REPO/apps/portal" exec vitest run \
.workflow-tests/INSIGHT-002_resolver_set_data.test.tsx \
.workflow-tests/INSIGHT-002_registry_can_name.test.tsx \
--reporter=verbose
Expected result:
Test Files 2 passed (2)
Tests 2 passed (2)
The Gist's COMPONENT_TEST_RECEIPT.txt preserves the original passing run. RESOLVER_EVM_RECEIPT.json, REGISTRY_EVM_DECISION_RECEIPT.json, and EVM_COMPARISON_RECEIPT.txt preserve the local-EVM consequence and complete-revoke controls. The tests and replays make no RPC request and perform no real wallet connection, signature, broadcast, public-network deployment, or production-state mutation.