Skip to content

fix: verify entityHash matches metadata in on-chain third-party validation#349

Merged
LautaroPetaccio merged 5 commits intomainfrom
fix/on-chain-third-party-entity-hash-verification
Apr 14, 2026
Merged

fix: verify entityHash matches metadata in on-chain third-party validation#349
LautaroPetaccio merged 5 commits intomainfrom
fix/on-chain-third-party-entity-hash-verification

Conversation

@LautaroPetaccio
Copy link
Copy Markdown
Contributor

Summary

The on-chain third-party asset validator accepts deployments without verifying that the entityHash in the merkle proof actually corresponds to the deployed metadata. This allows an attacker who owns any third-party entity to deploy arbitrary content by reusing a valid merkle proof from a different entity they own.

How the vulnerability works

The third-party validation flow has two paths: subgraph and on-chain. Both receive a deployment containing metadata and a merkle proof (entityHash, proof[], index). The correct verification is:

  1. Compute keccak256Hash(metadata, hashingKeys) from the actual deployed metadata
  2. Verify this computed hash matches merkleProof.entityHash (binding the proof to this specific metadata)
  3. Verify the proof against the on-chain merkle root

The subgraph path (subgraph/third-party-asset.ts) does all three steps correctly in its verifyHash() function. The on-chain path (on-chain/third-party-asset.ts) skipped step 2 — it used the user-provided merkleProof.entityHash directly in generateRoot() without checking it matched the metadata.

Attack scenario

  1. Attacker legitimately owns third-party emote A (registered in the merkle tree with hash H(A))
  2. Attacker wants to deploy unauthorized emote B (different content)
  3. Attacker submits emote B's metadata but includes emote A's merkle proof (with H(A))
  4. On-chain path computes the root using H(A) → root matches blockchain → validation passes
  5. Emote B is deployed with unauthorized content

For wearables, this is independently caught by thirdPartyWearableMerkleProofContentValidateFn in items/wearables.ts which re-hashes the metadata and verifies. But emotes have no equivalent check, making third-party emotes on the on-chain path exploitable.

What this PR does

Adds entity hash verification to verifyMerkleProofedEntity in the on-chain path, mirroring the subgraph path's verifyHash():

  • Validates the merkle proof structure with MerkleProof.validate()
  • Computes keccak256Hash(metadata, merkleProof.hashingKeys)
  • Rejects if the computed hash doesn't match merkleProof.entityHash

This ensures the proof is cryptographically bound to the deployed metadata regardless of entity type.

Test plan

  • npm run build compiles cleanly
  • npm test — all existing tests pass (the fix is additive; valid deployments still pass because their entityHash already matches)
  • Verify that a deployment with mismatched entityHash is rejected on the on-chain path

🤖 Generated with Claude Code

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 14, 2026

Test this pull request

  • The package can be tested by running
    yarn upgrade "https://sdk-team-cdn.decentraland.org/@dcl/content-validator/branch/fix/on-chain-third-party-entity-hash-verification/dcl-content-validator-7.1.1-24417244166.commit-8df1ed4.tgz"

@coveralls
Copy link
Copy Markdown

coveralls commented Apr 14, 2026

Coverage Status

coverage: 82.989% (-0.07%) from 83.062% — fix/on-chain-third-party-entity-hash-verification into main

…ation

The on-chain third-party asset validator was using the user-provided
entityHash directly without verifying it matched the actual metadata.
This could allow an attacker to deploy unauthorized content by reusing
a valid merkle proof from a different entity they own.

Add entityHash verification matching the subgraph path's verifyHash().
- Add test: modified metadata with reused merkle proof must fail
  because the entityHash in the proof won't match the keccak256
  hash of the tampered metadata
- Fix existing wearable test to assert response.ok is false when
  the checker contract fails (was only checking call counts)
@LautaroPetaccio LautaroPetaccio force-pushed the fix/on-chain-third-party-entity-hash-verification branch from 099eead to 6cef385 Compare April 14, 2026 17:19
Include the actual hash values in the log messages so operators can
diagnose proof mismatches without additional debugging. Matches the
logging pattern used by the subgraph third-party-asset validator.
Copy link
Copy Markdown

@decentraland-bot decentraland-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #349 — Security Fix for On-Chain Third-Party Entity Hash Verification

Verdict: ✅ APPROVE

This is a critical security fix that closes an exploitable gap in the on-chain third-party validation path. The fix is correct and well-scoped.

What the fix does

Adds the missing entityHash verification to verifyMerkleProofedEntity in the on-chain path, mirroring the subgraph path's existing verifyHash():

  1. MerkleProof.validate(merkleProof) — validates proof structure
  2. keccak256Hash(metadata, merkleProof.hashingKeys) — computes hash from actual metadata
  3. Rejects if computed hash ≠ merkleProof.entityHash — binds proof to metadata

Without this, the on-chain path used the user-supplied entityHash directly in generateRoot() without verifying it corresponded to the deployed metadata — allowing merkle proof reuse from a different entity.

Findings

P3 — Nice-to-Have:

  1. [Testing] The tampered-metadata test in emotes.spec.ts asserts response.ok is falsy but does not check the error message or verify that validateThirdParty was not called. Since other failure modes also produce ok: false, the test could pass for the wrong reason. Consider adding expect(components.L2.checker.validateThirdParty).not.toHaveBeenCalled() to confirm rejection happened at the hash check stage.

  2. [Testing] The existing wearables test fix (wearables.spec.ts) now properly asserts response.ok is falsy — good catch on the missing assertion.

CI Status

All checks passing. Coverage stable.

Summary

This fix is correct, necessary, and urgent. The emote path was fully exploitable via merkle proof reuse. Recommend merging promptly.

Requested by Lautaro Petaccio via Slack

The tampered-metadata test now verifies that validateThirdParty was
never called, confirming the rejection happened at the hash verification
stage. Without this assertion, the test could pass for the wrong reason
since other failure modes also produce ok: false.
Copy link
Copy Markdown

@decentraland-bot decentraland-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review — Focus on Breaking Changes & Lowercasing

No breaking change risk. For legitimate deployments, the entityHash in the merkle proof is computed from the actual metadata, so keccak256Hash(metadata, hashingKeys) will reproduce the same hash. Only tampered deployments (mismatched proof + metadata) will be rejected — which is the entire point.

No lowercasing concern. This PR doesn't involve any string case operations.

Security Assessment

This is a critical security fix. The on-chain path was missing the entity hash verification that the subgraph path already has. The test correctly validates the attack scenario (reusing a valid proof with tampered metadata).

One note

The MerkleProof.validate() call is a good addition — it provides structural validation before attempting the hash computation, preventing unexpected errors from malformed proof objects.

Verdict: Approve. Merge with high priority.


Requested by Lautaro Petaccio via Slack

@LautaroPetaccio LautaroPetaccio merged commit 29a7a52 into main Apr 14, 2026
3 of 4 checks passed
@LautaroPetaccio LautaroPetaccio deleted the fix/on-chain-third-party-entity-hash-verification branch April 14, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants