Weather Witness

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

Unauthorized NFT Minting via Missing Caller Validation in Fulfillment Flow

Root + Impact

The fulfillMintRequest function lacks caller validation, allowing:

  • Any address to trigger the minting process using a valid requestId.

  • The NFT to be minted to msg.sender (the arbitrary caller) instead of the original requester (_userMintRequest.user).

Example Attack Flow:

  1. Alice pays ETH and calls requestMintWeatherNFT (generating requestId=123).

  2. Bob front-runs Alice and calls fulfillMintRequest(123).

  3. Bob receives Alice’s NFT without paying ETH or initiating the request.


Impact

  • Critical Severity: Attackers can:

    • Steal NFTs intended for legitimate users.

    • Drain value from the system by minting unlimited NFTs at zero cost.

    • Disrupt user trust in the protocol’s core functionality.

  • Direct Violation of Business Logic: The minting process fails to enforce user-to-NFT ownership binding, rendering the payment mechanism (ETH mint fee) meaningless.


Likelihood

  1. Publicly Exposed Request IDs: The requestId generated during requestMintWeatherNFT is emitted in the WeatherNFTMintRequestSent event, making it visible on-chain. Attackers can trivially monitor these events to harvest valid requestId values.

  2. No Caller-Request Binding: The fulfillMintRequest function does not validate whether the caller owns the original mint request. Combined with the public requestId, this allows anyone to trigger the minting process for any pending request.


Impact

  1. Theft of Paid NFTs: Users who pay the ETH mint fee lose their NFTs to attackers who call fulfillMintRequest first.

  2. Protocol Fund Drain: Attackers mint NFTs at zero cost while legitimate users’ ETH payments are irrecoverably locked in the contract.


Proof of Concept (PoC)

Attack Scenario:

  1. Legitimate User Action:

    • Alice calls requestMintWeatherNFT, pays s_currentMintPrice ETH, and receives a requestId (e.g., 0x123).

  2. Attacker Action:

    • Bob monitors the WeatherNFTMintRequestSent event and extracts Alice’s requestId.

    • Bob immediately calls fulfillMintRequest(0x123).

  3. Result:

    • The NFT is minted to Bob’s address (msg.sender), not Alice’s.

    • Alice’s ETH payment is retained by the contract, but she receives no NFT.

// Attacker's contract or script
function stealNFT(bytes32 requestId) external {
weatherNft.fulfillMintRequest(requestId); // No ETH required
}

Key Observations:

  • Zero-Cost Exploit: Attackers pay only gas fees, bypassing the ETH mint fee.

  • Repeatable: The attack can be executed for every valid requestId emitted by the contract.

  • No Time Constraints: Attackers can exploit old/unfulfilled requests indefinitely.


Recommended Mitigation

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(msg.sender == _userMintRequest.user, "Unauthorized");
uint8 weather = abi.decode(response, (uint8));
uint256 tokenId = s_tokenCounter;
s_tokenCounter++;
_mint(_userMintRequest.user, tokenId);
s_tokenIdToWeather[tokenId] = Weather(weather);
// ... (keeper registration logic remains unchanged) ...
emit WeatherNFTMinted(requestId, _userMintRequest.user, Weather(weather));
}

New

Updates

Appeal created

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