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,
'"}'
)
)
)
);
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
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)
+ }