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

Private variables can be read directly from storage

Summary

The s_password variable is expected to be only known by the owner, however, anyone can read the password directly from the contract's storage.

Vulnerability Details

The string private s_password variable is expected to be only known by the owner of the contract, but private variables can be accessed in multiple different ways by accessing the storage slot the variables are stored in.

Here you can see the NatSpec above the contract that states "others won't be able to see" your password:

@notice This contract allows you to store a private password that others won't be able to see.

Here is a test easily showing that we can view the password by accessing PasswordStore's first storage slot. For emphasis, we even do so from address(1).

function test_non_owner_can_view_password() public {
// Owner sets a new password
string memory newPassword = "Froggy";
vm.prank(address(owner));
passwordStore.setPassword(newPassword);
// Load the first storage slot, where the password is declared at
vm.startPrank(address(1));
bytes32 data = vm.load(address(passwordStore), bytes32(uint256(1)));
// Convert the loaded data and the expected string to bytes
bytes memory loadedBytes = abi.encodePacked(data);
bytes memory expectedBytes = bytes(newPassword);
// Remove the padding and length data from `loadedBytes`
if (loadedBytes.length > expectedBytes.length) {
bytes memory trimmedBytes = new bytes(expectedBytes.length);
for (uint i = 0; i < expectedBytes.length; i++) {
trimmedBytes[i] = loadedBytes[i];
}
loadedBytes = trimmedBytes;
}
// Assert that the `loadedBytes` cast as a string is equal to `newPassword`
assertEq(string(loadedBytes), newPassword);
}

In this test, we first set the password to "Froggy", then using Foundry's vm.load() we store the data at PasswordStore's first storage slot in a variable called data. We can't directly compare the data to the newPassword string because of how Solidity stores dynamic variables, so before that we will cast both the newPassword and the data variables into bytes. After we create the two bytes variables, all that's left to do is remove the extra hex digits, such as padding and the string's length, from our loadedBytes object. To do so we use a simple for loop that creates a temporary trimmedBytes variable that contains all the loadedBytes data up until the length of loadedBytes is equal to expectedBytes, then we set loadedBytes equal to trimmedBytes, essentially updating the variable to exclude the extra hex digits we don't care about.

Finally, we use Foundry's assertEq() function to ensure that loadedBytes cast as a string is equal to newPassword.

Impact

This completely undermines the intentions of the developer and leaves the contract unusable if you truly care about the password being known by only the contract's owner.

Tools Used

Manual review

Recommendations

I recommend the developers entirely rethink their project and restructure it without trying to implement any sort of on-chain password since anything on the blockchain is publicly available for all to see. If you really must store a password in a smart contract, you need to hash/encode it before storing it, this way when anyone reads the variable from storage, they will only see the password's hash. One way to do so would be to use a one-way hashing algorithm such as SHA-256, just remember to never include the actual password in a blockchain transaction, or else it will be exposed forever.

Updates

Lead Judging Commences

inallhonesty Lead Judge
almost 2 years ago
inallhonesty Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-anyone-can-read-storage

Private functions and state variables are only visible for the contract they are defined in and not in derived contracts. In this case private doesn't mean secret/confidential

Support

FAQs

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