The mintProfile
function in the SoulboundProfileNFT.sol does not validate the profileImage
URL provided by users.
Since profileImage
is stored on-chain, it becomes immutable once minted, making exploitation permanent if malicious URLs are submitted.
So the Issues with the absence of validation on the profileImageUrl are
No validation of image format
Could accept unsupported formats
Potential display failures
Broken NFT previews
The issue is that the mintProfile function accepts the ProfileImage Url without sanitization and acts upon it by adding it in the base64 encoded metadata of the user
An attacker can set their profileImage
URL to a phishing page embeded in an attaractive image
https://fake-metamask.com/login.jpg
2.When users view this profile, they might be tricked into entering sensitive information, such as private keys or passwords. if this url is parsed
https://attacker.com/malware.jpg
when users view the profile they are exposed to malware
3.If the front end APP doesn't process the JSON string properly, such as using eval()
to parse token URI, then any malicious code can be executed in the front end. Obviously, funds in users' connected wallet, such as Metamask, might be stolen in this case.
POC:
a malicious user mints a soulbound NFT using the unvalidated profileImage e.g he uses a profile image of an attractive woman or a naked image that will serve as a click bait . Once an interested user shows interest by clicking the image to have a clearer look . the App as a whole would be exposed to malware or the user will also be exposed to phishing, malware or virus as the case may be
copy and paste this series of code in the SoulboundProfileNFT.t.sol
and run
first test passes a phishing link to collect sensitive details from another user
you will see that our test is passing and returning the base64 encoded metadata like this
Ran 1 test for test/testSoulboundProfileNFT.t.sol:SoulboundProfileNFTTest````[PASS] testMaliciousImageURL() (gas: 244261)````Traces:````[244261] SoulboundProfileNFTTest::testMaliciousImageURL()````├─ [0] VM::prank(0x0000000000000000000000000000000000000123)````│ └─ ← [Return]````├─ [211143] SoulboundProfileNFT::mintProfile("Alice", 25, "https://fake-metamask.com/login.jpg")````│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x0000000000000000000000000000000000000123, tokenId: 1)````│ ├─ emit ProfileMinted(user: 0x0000000000000000000000000000000000000123, tokenId: 1, name: "Alice", age: 25, profileImage: "https://fake-metamask.com/login.jpg")````│ └─ ← [Stop]````├─ [630] SoulboundProfileNFT::profileToToken(0x0000000000000000000000000000000000000123) [staticcall]````│ └─ ← [Return] 1````├─ [19233] SoulboundProfileNFT::tokenURI(1) [staticcall]````│ └─ ← [Return] "eyJuYW1lIjoiQWxpY2UiLCAiZGVzY3JpcHRpb24iOiJBIHNvdWxib3VuZCBkYXRpbmcgcHJvZmlsZSBORlQuIiwgImF0dHJpYnV0ZXMiOiBbeyJ0cmFpdF90eXBlIjogIkFnZSIsICJ2YWx1ZSI6IDI1fV0sICJpbWFnZSI6Imh0dHBzOi8vZmFrZS1tZXRhbWFzay5jb20vbG9naW4uanBnIn0="````├─ [0] VM::assertTrue(true, "Token URI should be set, but URL validation is missing") [staticcall]````│ └─ ← [Return]````└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.72ms (1.92ms CPU time)
Now let us copy the returned base64 and decode it somewhere else to see if our malicious input was utilized
The Result was:
{"name":"Alice", "description":"A soulbound dating profile NFT.", "attributes": [{"trait_type": "Age", "value": 25}], "image":"https://fake-metamask.com/login.jpg"}
you can see the json response returned our input
Storing a Fake Wallet Connection URL
Injecting JavaScript in an External URL
If the frontend automatically loads and displays images, an attacker can serve malicious JavaScript through a crafted URL.
like this
mintProfile("Eve", 28, "https://evil.com/image.svg?onload=stealWallet()");
When the user views the profile, JavaScript executes without their consent.
manual review , foundry
Modify the mintProfile
function to validate URLs before storing them on-chain:
Use a Whitelisted Image Storage Provider
Scamming/phishing is not the protocol problem, that's a user mistake. NFT are unique, even if someone does a copy of your profile (which is also possible in web2), I consider it informational. Injection is a problem for the web2 part of the protocol, not a bug here. For the age, it depends on the countries law and future medicine. Anyways, that's more an ethical/political problem, not a bug.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.