Sensitive Data Exposure
SCWE-044: Insecure Use of Storage
Theory
One of the most common misunderstandings for new Solidity developers is assuming that variables marked as private or placed inside constructors are hidden from users. This is incorrect: nothing stored on-chain is confidential.
Smart contracts store their persistent data inside EVM storage, and every full node maintains a complete, publicly accessible copy of it. Because the EVM does not provide built‑in confidentiality, any value written to storage can be retrieved, even if Solidity visibility modifiers forbid access from other contracts.
This leads to a broad vulnerability class known as Sensitive Data Exposure:
- private keys, passwords, salts, API secrets
- access control data
- whitelist lists or hashed values
EVM Memory Model Reminder
The EVM uses multiple data locations, each with different persistence and visibility rules:
.png)
| Location | Lifetime | Visibility | Purpose |
|---|---|---|---|
| Storage | Permanent, on-chain | Public (raw bytes) | Contract state |
| Memory | Temporary per call | Internal | Local variables, arrays |
| Stack | Per instruction | Internal | Expression evaluation |
| Calldata | Read-only input | Public | Function arguments from external calls |
| Logs | Permanent event logs | Public | Indexed historical data |
Only storage persists between transactions, making it the primary source of Sensitive Data Exposure risks. Although not permanently stored, sensitive information may still appear in transaction calldata, event logs, or deployment bytecode, depending on the contract's implementation.
Variable Visibility Is Not Data Protection
Solidity visibility modifiers (public, internal, private) are compile‑time constraints that restrict how Solidity code can access variables.
They do not:
- encrypt storage
- prevent external observers from reading storage slots
- hide data from RPC nodes or archive node tools
“Private” only means “other contracts cannot read it through Solidity syntax.”
Thus, the following variable is fully retrievable. “Private” only means “other contracts cannot read it through Solidity syntax.”
uint256 private secret;Account Storage Layout in Solidity (Technical Overview)
In the Ethereum Virtual Machine (EVM), contract storage is persistent across transactions and organized in deterministic 256-bit slots. This predictability means that if an attacker can reconstruct the layout, they can recover stored values.
Fixed-size variables are stored sequentially starting at slot 0. Solidity applies tight packing for variables smaller than 32 bytes, allowing multiple small variables to share the same slot. For example:
uint128 a;
uint128 b;
uint256 c;
// Storage layout:
// slot 0 -> contains a and b (packed together)
// slot 1 -> contains cMappings behave differently. A mapping declared at slot p stores each entry at keccak256(abi.encode(key, p)). While mappings are not enumerable, knowing or guessing keys allows an attacker to compute exact storage positions:
mapping(address => uint) balances;
// Entry for a key "0x123..." is at keccak256(abi.encode(0x123..., p))Dynamic arrays store their length in the declared slot (p), with data starting at keccak256(p). Each element i is located at keccak256(p) + i:
uint[] numbers;
// slot p -> length of the array
// slot keccak256(p) + i -> element iStrings and bytes have a dual storage scheme. Values ≤31 bytes are stored directly in the slot. Longer values store a length marker in the slot, with the actual data at keccak256(p):
bytes shortData; // ≤31 bytes, stored directly
bytes longData; // >31 bytes, slot contains length, data at keccak256(p)Short strings or bytes containing sensitive data are particularly exposed because they reside directly in storage slots.
Practice
Retrieving a “Private” Variable
In this contract, secret is the first declared state variable, which means it is stored at slot 0.
contract Vault {
bytes32 private secret = 0xdeadbeef; // supposed to be hidden
}Anyone can read storage slots directly from an RPC node.
Since we know secret is at slot 0x0, retrieving it simply means loading that slot. Following foundry script allows it.
// forge script scripts/ReadSecret.s.sol --rpc-url $RPC_URL -vv
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
contract ReadSecret is Script {
function run() external view {
address target = vm.envAddress("TARGET");
bytes32 slot0 = vm.load(target, bytes32(uint256(0)));
console.log("Secret:", uint256(slot0));
}
}Alternaltively we can use cast to retreive the data
cast storage $TARGET 0 --rpc-url $RPC_URL