tokenURI builds the profile's metadata JSON from a fixed protocol description ("A soulbound dating profile NFT.") and the stored name, Age attribute and image. The result should be well-formed JSON whose protocol-generated fields the profile owner cannot change.
mintProfile stores name and profileImage without any validation (L30, L37). tokenURI places them between literal quotes with abi.encodePacked (L93-L102) and escapes nothing. A " in profileImage closes the image string and lets the owner append extra members:
a duplicate description key, which last-key-wins parsers display instead of the protocol's;
new keys such as external_url or animation_url.
A " or \ in name produces invalid JSON, and honest users can type either character.
Likelihood:
Any profile owner can do it at mint, with no preconditions. An honest user whose name contains a double quote or a backslash triggers the invalid-JSON case without meaning to.
Standard wallets and explorers see the effect only once the separately reported missing data:application/json;base64, prefix is fixed. Until then, only consumers that base64-decode the value themselves parse this JSON.
Impact:
A profile owner can replace the protocol's fixed description (the PoC shows "Verified by DatingDapp"). They can also add keys such as external_url and animation_url with arbitrary links, and these appear as protocol-generated metadata. The PoC also overrides the Age trait, but that adds nothing new, because age is already an unvalidated mint input.
A double quote or backslash in a name makes that profile's metadata unparseable. The effect is limited to the owner's own soulbound token, and no funds are at risk.
The PoC deploys SoulboundProfileNFT and includes a small Base64 decoder, because OpenZeppelin 5.0.2 provides only encode. Every test checks that decoding and re-encoding gives back the exact tokenURI output. The baseline shows that an honest profile yields valid JSON with the protocol description and Age 25.
The injection test mints with a crafted profileImage. The ProfileMinted event confirms that the stored age is 25 and that the string was stored unescaped. The resulting JSON is valid, and Foundry's JSON parser:
resolves the duplicate keys to the attacker's values (description "Verified by DatingDapp", Age 99);
cuts image to ipfs://x.png;
finds the added external_url and animation_url.
The third test shows that vm.parseJsonString fails on two names where the baseline parses: one containing double quotes, and one containing a backslash (C:\Users). Last-key-wins was shown with Foundry's parser only. JavaScript's JSON.parse also keeps the last duplicate, but that was not run here.
Run from the repository root, with the file saved as test/PoC_DD-09.t.sol:
Result: Suite result: ok. 3 passed; 0 failed; 0 skipped. The logs show parsed description : Verified by DatingDapp and injected external_url: https://phish.example.
Reject characters that can break out of a JSON string, or escape them in tokenURI.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.