Skip to content

M-04: _approve Permits Recording Approvals for Non-existent Tokens, Breaking Downstream Circuits #798

Description

@andrew-fleming

M-04 _approve Permits Recording Approvals for Non-existent Tokens, Breaking Downstream Circuits

Severity: Medium

Source: Midnight Foundation #2 — OpenZeppelin Compact Contracts Audit, Release 0.3.0-alpha.1 (2026-08-18)


The exported _approve circuit of NonFungibleToken performs its approver validation only when the auth argument is non-zero: the token-existence check sits inside the guarded branch, while the approval write executes unconditionally. The zero-auth path is documented as a deliberate escape hatch, and its requirements list no condition that tokenId exist.

The mock contract MockNonFungibleTokenexports the _approve circuit, bypassing the generation of auth through computeAccountId as implemented in approve circuit. Consequently, following the mock contract implementation, an approval can be recorded for a token that does not exist, and the invariant that a populated approval implies an existing token no longer holds. That invariant is exactly what the module relies upon elsewhere: a comment in _unsafeTransferFrom states that supplying an auth argument verifies token existence and that the returned previous owner therefore need not be checked against zero. Two exploits follow. In the first, an approval is planted on an unminted identifier and the approvee calls the public transferFrom or _unsafeTransferFrom with the zero account as the source: the update circuit reads the owner as zero, passes authorization on the stale approval, skips the balance decrement, credits the recipient and writes the owner entry, minting the token without mint authorization and permanently blocking the composer's own gated mint of that identifier. In the second, an approval is planted on an identifier about to be minted legitimately; because the update circuit clears approvals only when the source is non-zero, a mint never clears the approval, so the planted approval survives and the planter transfers the freshly minted token away from its owner.

Consider enforcing token existence independently of approval state. For example, make _checkAuthorized revert for owner == Utils_zeroAccount() unconditionally (before evaluating approvals), and explicitly reject fromAddress == Utils_zeroAccount() in transferFrom / _unsafeTransferFrom. In addition, consider updating the mock contracts by removing all internal circuits that are not supposed to be exported to avoid integration issues for composers extending the contract.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

1-mediumNormal priorityauditIssues reported by an audit

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions