Summary
The MetadataImage
contract generates SVG images for NFTs, incorporating token symbols. However, it doesn't escape special characters in these symbols, potentially resulting in invalid SVG output. This can cause rendering issues or complete failure to display the NFT image correctly.
Vulnerability Details
https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/4e0ad0b964f74a1b4880114f4dd5b339bc69cd3e/protocol/contracts/beanstalk/metadata/MetadataImage.sol#L647-L655
In the tokenName
and getTokenName
functions, the contract directly inserts the token symbol into the SVG XML without escaping special characters:
function tokenName(address token) internal view returns (string memory) {
return
string(
abi.encodePacked(
'<text x="10" y="14.5" font-size="12" fill="White" text-anchor="start" font-family="futura">',
getTokenName(token),
"</text>"
)
);
}
function getTokenName(address token) internal view returns (string memory tokenString) {
if (token == C.UNRIPE_LP) {
tokenString = "urBEANLP";
} else {
tokenString = ERC20(token).symbol();
}
}
If a token's symbol contains special XML characters like <
, >
, &
, '
, or "
, it will break the SVG structure, potentially causing rendering issues or making the SVG invalid.
Impact
NFT images may fail to render correctly or at all in various SVG viewers or browsers.
Important token information (symbol) might be missing or displayed incorrectly.
User experience degradation due to broken or improperly rendered NFT images.
Potential confusion or misinformation about the nature of the NFT due to missing or incorrect token symbols.
Possible issues with third-party platforms or marketplaces that process these NFTs.
Reputational damage to the project due to consistently broken or incorrectly rendered NFTs.
Tools Used
Recommendations
Implement an XML/SVG escaping function:
function escapeXML(string memory input) internal pure returns (string memory) {
bytes memory inputBytes = bytes(input);
bytes memory output = new bytes(inputBytes.length * 6);
uint256 outputLength = 0;
for (uint256 i = 0; i < inputBytes.length; i++) {
if (inputBytes[i] == '&') {
bytes memory escaped = "&";
for (uint256 j = 0; j < escaped.length; j++) {
output[outputLength++] = escaped[j];
}
} else if (inputBytes[i] == '<') {
bytes memory escaped = "<";
for (uint256 j = 0; j < escaped.length; j++) {
output[outputLength++] = escaped[j];
}
} else if (inputBytes[i] == '>') {
bytes memory escaped = ">";
for (uint256 j = 0; j < escaped.length; j++) {
output[outputLength++] = escaped[j];
}
} else if (inputBytes[i] == '"') {
bytes memory escaped = """;
for (uint256 j = 0; j < escaped.length; j++) {
output[outputLength++] = escaped[j];
}
} else if (inputBytes[i] == "'") {
bytes memory escaped = "'";
for (uint256 j = 0; j < escaped.length; j++) {
output[outputLength++] = escaped[j];
}
} else {
output[outputLength++] = inputBytes[i];
}
}
bytes memory result = new bytes(outputLength);
for (uint256 i = 0; i < outputLength; i++) {
result[i] = output[i];
}
return string(result);
}
Modify the getTokenName
function to use this escaping function:
function getTokenName(address token) internal view returns (string memory tokenString) {
if (token == C.UNRIPE_LP) {
tokenString = "urBEANLP";
} else {
tokenString = escapeXML(ERC20(token).symbol());
}
}