Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
Submission Details
Impact: medium
Likelihood: medium
Invalid

Potentially sensitive data stored on chain (in b64)

Author Revealed upon completion

Root + Impact

Description

  • NFT metadata is typically stored off-chain (e.g., on IPFS or via centralized servers), with URIs pointing to those resources. In Snowman.sol, the complete metadata, including an embedded SVG image or image URI, is base64 encoded and stored directly on-chain via the tokenURI function.

  • This behavior leads to permanent storage of potentially sensitive or large data in an immutable public space, increasing gas costs ( a big one), compromising privacy, and hindering upgradeability.

  • The main concern in terms of sensitivity is the s_SnowmanSvgUri variable, which is:

    1. Stored permanently on-chain as a state variable (set in constructor)

    1. Embedded in every NFT's metadata and base64 encoded

    1. Publicly readable by anyone

Additionally:

Potential Sensitivity Issues:

  1. SVG Content: If s_SnowmanSvgUri contains a full SVG data URI (like data:image/svg+xml;base64,[encoded-svg]), the entire SVG content becomes permanently stored on-chain in base64 format.

  1. External URLs: If it contains external URLs, those URLs are permanently recorded on-chain, potentially revealing:

  • Server infrastructure details

  • API endpoints

  • Storage locations (IPFS hashes, etc.)

  1. Metadata Permanence: All NFT metadata (name, description, attributes) is permanently stored on-chain in base64 format, making it immutable and publicly accessible forever.

// Root cause in the codebase with @> marks to highlight the relevant section
function tokenURI(uint256 tokenId) public view override returns (string memory) {
...
string memory json = Base64.encode(
bytes(
string(
abi.encodePacked(
'{"name": "Snowman Airdrop", "description": "Snowman for everyone!!!", ',
'"attributes": [{"trait_type": "freezing", "value": 100}], "image": "',
@> s_SnowmanSvgUri,
'"}'
)
)
)
);
...
}

Risk

Likelihood:

  • This will occur every time tokenURI is called, which happens on any frontend or marketplace metadata lookup.

  • The s_SnowmanSvgUri variable is embedded in every NFT's on-chain metadata and is initialized in the constructor with no method for later modification.

Impact:

  • The SVG image or external resource URI becomes permanently and publicly stored on-chain, exposing infrastructure details or sensitive data if present.

  • Increased gas cost and contract size from storing full metadata and SVG data on-chain, affecting deployment and minting costs.

Proof of Concept

Explanation of POC:
This PoC demonstrates how the contract embeds an unchangeable s_SnowmanSvgUri into every token's metadata,
encoding the entire JSON metadata—including image URI—on-chain via base64.
This design leads to permanent public exposure of potentially sensitive or bulky data and incurs high gas
costs due to on-chain metadata storage.
constructor(string memory svgUri) {
s_SnowmanSvgUri = svgUri;
}
function tokenURI(uint256 tokenId) public view override returns (string memory) {
string memory json = Base64.encode(
bytes(
string(
abi.encodePacked(
'{"name": "Snowman Airdrop", "description": "Snowman for everyone!!!", ',
'"attributes": [{"trait_type": "freezing", "value": 100}], "image": "',
s_SnowmanSvgUri,
'"}'
)
)
)
);
return string(abi.encodePacked("data:application/json;base64,", json));
}

Recommended Mitigation

explanation:
- remove this code
+ add this code
- Store full metadata and SVG/image data on-chain
+ Store only a pointer (e.g., IPFS CID) to external metadata that includes the image URI
+ Ensure sensitive infrastructure or internal URLs are not embedded in contract state variables
+ Provide a setter or versioning mechanism if mutability of metadata is desired
mitigation in code:
// >>> PUBLIC FUNCTIONS
+ // Fixed: Simple IPFS URI concatenation instead of expensive base64 encoding
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
if (ownerOf(tokenId) == address(0)) {
revert ERC721Metadata__URI_QueryFor_NonExistentToken();
}
- string memory imageURI = s_SnowmanSvgUri;
-
- return string(
- abi.encodePacked(
- _baseURI(),
- Base64.encode(
- abi.encodePacked(
- '{"name":"',
- name(),
- '", "description":"Snowman for everyone!!!", ',
- '"attributes": [{"trait_type": "freezing", "value": 100}], "image":"',
- imageURI,
- '"}'
- )
- )
- )
- );
+
+ // Simple concatenation: baseURI + tokenId + ".json"
+ return string(abi.encodePacked(s_BaseMetadataURI, _toString(tokenId), ".json"));
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
4 days ago
yeahchibyke Lead Judge 1 day ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.