Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

lack of acess-control on "santa's" checkList

Summary

The SantasList contract assumes that "only " santa can modify his list. The checkList() function modifies the s_theListCheckedOnce storage variable, where santa puts an address on his list and changes status, but the function doesn't include access control meaning that anyone, including a malicious actor, can put any address or their own, or change status.

Vulnerability Details

This vulnerability exists in the checkList() function in the SantasList.sol file starting line 121

The checkList function includes no access controls meaning that anyone can call it and modify the list:

/*
* @notice Do a first pass on someone if they are naughty or nice.
* Only callable by santa
*
* @param person The person to check
* @param status The status of the person
*/
@> function checkList(address person, Status status) external {
s_theListCheckedOnce[person] = status;
emit CheckedOnce(person, status);
}

To restrict who can modify the list, there needs to be a check that the function caller, the msg.sender, is the owner of the contract, i.e. i_santa

Impact

The checkList function in the SantasList.sol contract allows anyone to update the list, which could lead to unauthorized changes or malicious activities.

For example, a malicious actor could add their own name to the list, or change the status of someone else's name to naughty. This could have a significant impact on the integrity of the list and could lead to people being unfairly rewarded or punished.

As per the following NatSpec comment: Only callable by santa i.e. only i_santa the owner can modify the list is the core assumption, and functionality that does not hold, this is a high severity vulnerability.

Proof of Concept

Working Test Case

The makeAddr helper function is used to setup an attacker address to call the checkList() function:

+ address attacker = makeAddr("attacker");
address santa = makeAddr("santa");
function setUp() public {
+ vm.startPrank(attacker);
santasList = new SantasList();
vm.stopPrank();
}
}

The following test, will set an address "attacker" and change the status to NICE. When run, this test will pass, demonstrating that the attacker can modify the list:

function testCheckList() public {
vm.prank(attacker);
santasList.checkList(attacker, SantasList.Status.NICE);
assertEq(uint256(santasList.getNaughtyOrNiceOnce(attacker)), uint256(SantasList.Status.NICE));
}
}

Run the test:

forge test --match-path test/unit/SantasCheckListTest.t.sol -vvvv

Which yields the following output:

Running 1 test for test/unit/SantasCheckListTest.t.sol:SantasListTest
[PASS] testCheckList() (gas: 15957)
Traces:
[15957] SantasListTest::testCheckList()
├─ [0] VM::prank(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e])
│ └─ ← ()
├─ [4211] SantasList::checkList(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e], 0)
│ ├─ emit CheckedOnce(person: attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e], status: 0)
│ └─ ← ()
├─ [690] SantasList::getNaughtyOrNiceOnce(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e]) [staticcall]
│ └─ ← 0
└─ ← ()
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.36ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommendation Mitigation

Include access control to restrict who can call the checkList function to be only the owner: i_santa. This can be achieved by 2 ways:

  • using the onlySanta modifier

// @audit check that the function caller is the owner of the contract using the modifier
+ function checkList(address person, Status status) external onlySanta {
s_theListCheckedOnce[person] = status;
emit CheckedOnce(person, status);
}
  • Using an access modifier e.g. OpenZeppelin's onlyOwner. To use this modifier, the SantasList contract will need to inherit from OpenZeppelin's Ownable contract and call it's constructor inside the constructor of SantasList:

// @audit import the ownable contract from OpenZeppelin
+ import "@openzeppelin/contracts/ownership/Ownable.sol";
// @audit inherit from the Ownable contract
+ contract SantasList is ERC721, TokenUri, Ownable {
error SantasList__NotSanta();
//SNIP
+ constructor() Ownable() {
s_owner = msg.sender;
}
}

As per the OpenZeppelin documentation, by default, the owner of an Ownable contract is the account that deployed it, meaning that the s_owner state variable can be removed.

Using onlyOwner modifier(similar to the onlySanta modifier) adds logic to check that the msg.sender is the owner of the contract before executing the function's logic:

/*
* @notice This function allows only the owner to modify a list.
*/
+ function checkList(address person, Status status) external onlyOwner {
s_theListCheckedOnce[person] = status;
emit CheckedOnce(person, status);
}

Tools Used

  • Forge

Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Access Control on checkList()

Anyone is able to call checkList() changing the status of a provided address. This is not intended functionality and is meant to be callable by only Santa.

Support

FAQs

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