Summary
The SoulboundProfileNFT
contract in this dating platform allows any arbitrary URL for profile images, enabling attackers to inject malicious scripts and conduct XSS attacks through profile metadata.
Vulnerability Details
contract SoulboundProfileNFT {
struct Profile {
string name;
uint8 age;
- string profileImage; // No URL validation
}
function mintProfile(
string memory name,
uint8 age,
- string memory profileImage // Accepts any URL
) external {
require(profileToToken[msg.sender] == 0, "Profile already exists");
uint256 tokenId = ++_nextTokenId;
_safeMint(msg.sender, tokenId);
_profiles[tokenId] = Profile(name, age, profileImage);
}
}
POC
All malicious URLS are accepted
function testMaliciousURLs() public {
string[] memory attacks = new string[](3);
attacks[0] = "javascript:alert('xss')";
attacks[1] = "<script>alert('hack')</script>";
attacks[2] = "data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGFjaycpPC9zY3JpcHQ+";
for(uint i = 0; i < attacks.length; i++) {
vm.startPrank(address(uint160(i + 1)));
soulboundNFT.mintProfile("Attack", 25, attacks[i]);
vm.stopPrank();
}
}
Impact
-
XSS attacks through profile images
-
Malicious script injection
-
Display of malicious content
Recommendations
Adding a util function that validates URLS from the user.
string public constant IPFS_PREFIX = "ipfs://";
function isValidIPFSUrl(string memory url) internal pure returns (bool) {
bytes memory urlBytes = bytes(url);
bytes memory prefix = bytes(IPFS_PREFIX);
if (urlBytes.length < prefix.length) return false;
for (uint i = 0; i < prefix.length; i++) {
if (urlBytes[i] != prefix[i]) return false;
}
return true;
}
contract SoulboundProfileNFT {
struct Profile {
string name;
uint8 age;
+ string profileImage; // Should be IPFS only
}
function mintProfile(
string memory name,
uint8 age,
+ string memory profileImage // Should validate IPFS format
) external {
require(profileToToken[msg.sender] == 0, "Profile already exists");
+ require(isValidIPFSUrl(profileImage), "Invalid image URL");
uint256 tokenId = ++_nextTokenId;
_safeMint(msg.sender, tokenId);
_profiles[tokenId] = Profile(name, age, profileImage);
}
}