Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: low
Likelihood: high
Invalid

Broken JSON Metadata

Root + Impact

The tokenURI function directly embeds the s_SnowmanSvgUri string into a JSON template without escaping special characters. If this URI contains double quotes ("), the resulting JSON becomes malformed. Off-chain platforms (marketplaces, wallets) cannot parse it, so the token image and attributes fail to display

Description

  • Under normal conditions, tokenURI returns a valid base64-encoded JSON metadata string that includes the token name, description, attributes, and an image URI. This metadata is consumed by applications like OpenSea to show the NFT correctly.

    The problem occurs because the contract builds the JSON by concatenating raw strings:

return string(
abi.encodePacked(
_baseURI(),
Base64.encode(
abi.encodePacked(
'{"name":"',
name(),
'", "description":"Snowman for everyone!!!", ',
'"attributes": [{"trait_type": "freezing", "value": 100}], "image":"',
imageURI, // @> s_SnowmanSvgUri is inserted here directly
'"}'
)
)
)
);

If s_SnowmanSvgUri contains a double quote (e.g., inside an SVG attribute like viewBox="0 0 100 100"), it prematurely closes the "image" field, breaking the JSON structure.

Risk

Likelihood:

  • High: The SVG URI is set by the contract owner and may legitimately include double quotes (e.g., when the SVG is stored as a data URI with inline attributes).

  • Any NFT minted with such a URI will produce broken metadata.

Impact:

  • The token’s image does not appear on marketplaces and wallets.

  • Attributes and other metadata fields become unreadable, degrading the user experience and potentially harming the project’s reputation.

  • No on-chain funds or contract logic are affected.

Proof of Concept

The following Foundry test demonstrates the issue. It deploys the contract with a malicious SVG URI containing double quotes, mints a token, and attempts to parse the resulting JSON – which fails

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "openzeppelin-contracts/contracts/utils/Base64.sol";
import "../src/Snowman.sol";
struct MetadataSimple {
string name;
string description;
string image;
}
contract SnowmanTest is Test {
Snowman public snowman;
address public receiver;
string public maliciousSvgUri = '<svg viewBox="0 0 100 100">...</svg>';
string public safeSvgUri = '<svg viewBox=\'0 0 100 100\'>...</svg>';
function setUp() public {
receiver = makeAddr("receiver");
}
function parseJsonString(string memory jsonString) external pure returns (MetadataSimple memory) {
bytes memory jsonData = vm.parseJson(jsonString);
return abi.decode(jsonData, (MetadataSimple));
}
function test_SafeURI() public {
snowman = new Snowman(safeSvgUri);
snowman.mintSnowman(receiver, 1);
string memory tokenUri = snowman.tokenURI(0);
string memory baseUri = "data:application/json;base64,";
string memory jsonBase64 = vm.replace(tokenUri, baseUri, "");
bytes memory jsonBytes = Base64.decode(jsonBase64);
string memory jsonString = string(jsonBytes);
console.log("JSON string:", jsonString);
MetadataSimple memory metadata = this.parseJsonString(jsonString);
assertEq(metadata.image, safeSvgUri, "Image URI mismatch");
}
function test_BrokenJSON() public {
snowman = new Snowman(maliciousSvgUri);
snowman.mintSnowman(receiver, 1);
string memory tokenUri = snowman.tokenURI(0);
string memory baseUri = "data:application/json;base64,";
string memory jsonBase64 = vm.replace(tokenUri, baseUri, "");
bytes memory jsonBytes = Base64.decode(jsonBase64);
string memory jsonString = string(jsonBytes);
vm.expectRevert();
this.parseJsonString(jsonString);
}
}

Running the test confirms that the JSON is invalid, proving the vulnerability.

Recommended Mitigation

Escape any double quotes inside the image URI before embedding it into the JSON. A simple way is to replace " with \". Alternatively, store the SVG as a base64-encoded data URI to avoid quotes altogether.

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;
+ // Escape double quotes in the imageURI
+ imageURI = _escapeJSONString(imageURI);
return string(
abi.encodePacked(
_baseURI(),
Base64.encode(
abi.encodePacked(
'{"name":"',
name(),
'", "description":"Snowman for everyone!!!", ',
'"attributes": [{"trait_type": "freezing", "value": 100}], "image":"',
imageURI,
'"}'
)
)
)
);
}
+ function _escapeJSONString(string memory str) internal pure returns (string memory) {
+ // Implementation can loop and replace " with \"
+ // (Full implementation omitted for brevity, but critical)
+ }
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 7 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!