Skip to content

Fix/v2 metadata deserialize cursor#4839

Open
0x4ka5h wants to merge 3 commits into
otter-sec:anchor-nextfrom
Otter-0x4ka5h:fix/v2-metadata-deserialize-cursor
Open

Fix/v2 metadata deserialize cursor#4839
0x4ka5h wants to merge 3 commits into
otter-sec:anchor-nextfrom
Otter-0x4ka5h:fix/v2-metadata-deserialize-cursor

Conversation

@0x4ka5h

@0x4ka5h 0x4ka5h commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

finding

The metadata account wrappers in spl-v2 deserialized from a copied slice and did not advance the caller's input cursor. That contradicts the AccountDeserialize contract and can misalign chained deserialization.

Fix

Implemented cursor-advancing deserialization for MetadataAccount, MasterEditionAccount, and TokenRecordAccount. This restores the expected AccountDeserialize semantics while preserving the existing type checks.

@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the OtterSec Team on Vercel.

A member of the Team first needs to authorize it.

@hacktron-app hacktron-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 1 file

Severity Count
HIGH 1

View full scan results

Comment thread spl-v2/src/metadata.rs
Comment on lines +758 to +764
let (token_standard, collection, uses) =
match (token_standard_res, collection_res, uses_res) {
(Ok(token_standard), Ok(collection), Ok(uses)) => {
(token_standard, collection, uses)
}
_ => (None, None, None),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH Loss of Token Standard and Collection Fields in Metadata Deserialization for Older Accounts

The custom MetadataAccount::parse implementation introduced in this PR manually deserializes the optional fields of a Metaplex Metadata account. However, it groups the deserialization results of token_standard, collection, and uses into a single match statement:

let (token_standard, collection, uses) =
    match (token_standard_res, collection_res, uses_res) {
        (Ok(token_standard), Ok(collection), Ok(uses)) => {
            (token_standard, collection, uses)
        }
        _ => (None, None, None),
    };

If any of these three fields fails to deserialize (for example, if an older metadata account is shorter and does not contain the uses field, causing uses_res to return an Err due to EOF), the entire match falls back to the wildcard pattern _ => (None, None, None).

This causes successfully deserialized token_standard and collection fields to be silently discarded and set to None for any older metadata accounts.

Attack Scenario & Impact

  1. Royalty/Ruleset Bypass: If a Programmable NFT (pNFT) is stored in an older account format that lacks the uses field, its token_standard will be parsed as None instead of Some(TokenStandard::ProgrammableNonFungible). Downstream programs (such as marketplaces) that rely on this helper to detect pNFTs will treat it as a legacy NFT, allowing users to bypass royalty enforcement and ruleset checks.
  2. Collection Verification Failure: Any downstream program checking metadata.collection to verify that an NFT belongs to a specific collection will see None and reject the NFT, causing a Denial of Service (DoS) for older NFTs.
Steps to Reproduce
An older Metaplex Metadata account with a valid `token_standard` and `collection` but no `uses` field (EOF) is passed to `MetadataAccount::try_deserialize`. The function successfully parses `token_standard` and `collection`, but because `uses_res` is `Err(EOF)`, the match discards both successfully parsed fields and returns them as `None`.
Fix with AI

Open in Cursor Open in Claude

A security vulnerability was found by Hacktron.

File: spl-v2/src/metadata.rs
Lines: 758-764
Severity: high

Vulnerability: Loss of Token Standard and Collection Fields in Metadata Deserialization for Older Accounts

Description:
The custom `MetadataAccount::parse` implementation introduced in this PR manually deserializes the optional fields of a Metaplex `Metadata` account. However, it groups the deserialization results of `token_standard`, `collection`, and `uses` into a single `match` statement:

```rust
let (token_standard, collection, uses) =
    match (token_standard_res, collection_res, uses_res) {
        (Ok(token_standard), Ok(collection), Ok(uses)) => {
            (token_standard, collection, uses)
        }
        _ => (None, None, None),
    };
```

If any of these three fields fails to deserialize (for example, if an older metadata account is shorter and does not contain the `uses` field, causing `uses_res` to return an `Err` due to EOF), the entire match falls back to the wildcard pattern `_ => (None, None, None)`.

This causes successfully deserialized `token_standard` and `collection` fields to be silently discarded and set to `None` for any older metadata accounts.

### Attack Scenario & Impact
1. **Royalty/Ruleset Bypass**: If a Programmable NFT (pNFT) is stored in an older account format that lacks the `uses` field, its `token_standard` will be parsed as `None` instead of `Some(TokenStandard::ProgrammableNonFungible)`. Downstream programs (such as marketplaces) that rely on this helper to detect pNFTs will treat it as a legacy NFT, allowing users to bypass royalty enforcement and ruleset checks.
2. **Collection Verification Failure**: Any downstream program checking `metadata.collection` to verify that an NFT belongs to a specific collection will see `None` and reject the NFT, causing a Denial of Service (DoS) for older NFTs.

Proof of Concept:
An older Metaplex Metadata account with a valid `token_standard` and `collection` but no `uses` field (EOF) is passed to `MetadataAccount::try_deserialize`. The function successfully parses `token_standard` and `collection`, but because `uses_res` is `Err(EOF)`, the match discards both successfully parsed fields and returns them as `None`.

Affected Code:
        let (token_standard, collection, uses) =
            match (token_standard_res, collection_res, uses_res) {
                (Ok(token_standard), Ok(collection), Ok(uses)) => {
                    (token_standard, collection, uses)
                }
                _ => (None, None, None),
            };

Acceptance criteria:
- Acceptance is defined by the **actual reported behavior**, not by tests passing.
- Reproduce the issue, or narrow the exact code path that produces it, *before* changing code. State what you confirmed.
- Fix the underlying cause. Mitigations that paper over the reported behavior do not count as a fix.
- Add a regression test that fails on the unpatched code and passes on the fix. If a regression test is genuinely impractical (e.g. race condition, infra-level issue), say so and explain why.
- Existing tests passing is **not** the bar. Do not declare done on tests-pass theatre.

Only change what is necessary to fix this vulnerability. Do not refactor adjacent code or modify unrelated files.

Triage: Reply !fp <reason> (false positive), !valid (confirmed), !accepted_risk <reason>, or !fixed (resolved). Any other reply is saved as a triage note.
Reason is optional but improves future scans — e.g. !fp internal endpoint, not user-facing.

View finding in Hacktron

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

!fp The grouped fallback in MetadataAccount::parse only drops token_standard, collection, and uses together when the trailing v1.2 fields are partially truncated or otherwise malformed

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (anchor-next@e056cc1). Learn more about missing BASE report.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@              Coverage Diff               @@
##             anchor-next    #4839   +/-   ##
==============================================
  Coverage               ?   63.61%           
==============================================
  Files                  ?      126           
  Lines                  ?    13666           
  Branches               ?        0           
==============================================
  Hits                   ?     8694           
  Misses                 ?     4972           
  Partials               ?        0           
Flag Coverage Δ
v2 63.61% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread spl-v2/src/metadata.rs
return Err(ProgramError::InvalidAccountData);
}

let key: Key =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit — document the on-chain order in here while deserializing.

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