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:
Stored permanently on-chain as a state variable (set in constructor)
Embedded in every NFT's metadata and base64 encoded
Publicly readable by anyone
Additionally:
Potential Sensitivity Issues:
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.
External URLs: If it contains external URLs, those URLs are permanently recorded on-chain, potentially revealing:
Metadata Permanence: All NFT metadata (name, description, attributes) is permanently stored on-chain in base64 format, making it immutable and publicly accessible forever.
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"));
}