Pieces Protocol

First Flight #32
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: medium
Invalid

Unvalidated Callback Data in onERC721Received

Summary

The TokenDivider contract's onERC721Received function accepts and ignores arbitrary callback data, creating a potential attack vector

Vulnerability Details

The function accepts any callback data without validation:

// In TokenDivider.sol
function onERC721Received(
address, /* operator */
address, /* from */
uint256, /* tokenId */
bytes calldata /* data */
) external pure override returns (bytes4) {
return this.onERC721Received.selector;
}

Impact

  • Creates potential attack vector for future upgrades

  • Could be used for malicious callback execution

  • No validation of transfer inten

POC

First, the malicious contract used for the attack:

// test/mocks/MaliciousNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {TokenDivider} from "../../src/TokenDivider.sol";
contract MaliciousNFT is ERC721 {
TokenDivider public dividerContract;
uint256 private constant TOKEN_ID = 0;
constructor() ERC721("MaliciousNFT", "MNFT") {}
function mint(address to) public {
_mint(to, TOKEN_ID);
}
// Override safeTransferFrom to add malicious behavior
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes memory data
) public override {
// Try to execute malicious callback data
(bool success,) = to.call(data);
require(!success, "Malicious callback succeeded");
super.safeTransferFrom(from, to, tokenId, data);
}
}

Complete test setup showing the vulnerability:

// test/unit/TokenDividerReceiveTest.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {Test, console} from "forge-std/Test.sol";
import {TokenDivider} from "../../src/TokenDivider.sol";
import {MaliciousNFT} from "../mocks/MaliciousNFT.sol";
contract TokenDividerReceiveTest is Test {
TokenDivider public divider;
MaliciousNFT public maliciousNft;
address public constant ATTACKER = address(0x2);
uint256 public constant TOKEN_ID = 0;
function setUp() public {
divider = new TokenDivider();
maliciousNft = new MaliciousNFT();
maliciousNft.mint(ATTACKER);
}
function test_MaliciousCallback() public {
console.log("\n=== Testing Malicious Callback in onERC721Received ===");
vm.startPrank(ATTACKER);
console.log("Attacker address:", ATTACKER);
console.log("\nAttempting to trigger onERC721Received with malicious data...");
bytes memory maliciousData = abi.encodeWithSignature(
"maliciousFunction(address,uint256)",
ATTACKER,
TOKEN_ID
);
maliciousNft.safeTransferFrom(ATTACKER, address(divider), TOKEN_ID, maliciousData);
console.log("\nVULNERABILITY: Contract accepted transfer with malicious callback data!");
console.log("Malicious data was ignored but transfer succeeded");
console.log("Current NFT owner:", maliciousNft.ownerOf(TOKEN_ID));
vm.stopPrank();
}
}
=== Testing Malicious Callback in onERC721Received ===
Attacker address: 0x0000000000000000000000000000000000000002
Attempting to trigger onERC721Received with malicious data...
VULNERABILITY: Contract accepted transfer with malicious callback data!
Malicious data was ignored but transfer succeeded
Current NFT owner: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f

Tools Used

  • Manual review

Recommendations

  1. Validate callback data:

function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external override returns (bytes4) {
require(data.length == 0, "No callback data allowed");
return this.onERC721Received.selector;
}

If callback data is needed, implement strict validation:

function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external override returns (bytes4) {
if (data.length > 0) {
require(
keccak256(data) == keccak256(abi.encode("dividenft")),
"Invalid callback data"
);
}
return this.onERC721Received.selector;
}
Updates

Lead Judging Commences

fishy Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.