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:

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_URLExtracting Mapping Entries
Mappings introduce a layer of indirection, but their storage layout is still fully deterministic. If an attacker knows the key, they can compute the exact slot where the value is stored.
contract Registry {
mapping(address => uint256) private balances;
}
A mapping declared at slot p stores each value at:
keccak256(abi.encode(key, p))Here p = 0, because balances is the first variable.
If we want to read the balance for address k, we simply compute
keccak256(abi.encode(k, 0)) and load that slot. Following foundry script allows it.
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
contract ReadMapping is Script {
function run() external view {
address target = vm.envAddress("TARGET");
address user = vm.envAddress("USER"); // The key (address)
bytes32 slot = keccak256(abi.encode(user, uint256(0)));
bytes32 val = vm.load(target, slot);
console.log("Balance:", uint256(val));
}
}This will print the user’s “private” balance. Mappings are not enumerable, but when keys are known (e.g., user addresses), reconstructing entries is trivial.
Alternaltively we can use cast to retreive the data
# 1. Compute the storage slot for a given address (user)
cast keccak $(cast abi-encode "(address,uint256)" $USER 0)
# 2. Read the value at that slot (with $SLOT the previous result)
cast storage $TARGET $SLOT --rpc-url $RPC_URLConstructor Secrets Are Public
A surprisingly common misconception is that constructor arguments are invisible once deployment is complete. In reality, constructor arguments are included in the deployment transaction calldata, which becomes permanently accessible on-chain. This exposes secrets even before they reach storage.
contract APIKeyHolder {
string private apiKey;
constructor(string memory key) {
apiKey = key; // assumed private
}
}Even though the apiKey variable is private and later stored using Solidity’s string layout, the secret already leaked during deployment.
// PoC Todo:
Retrieve the contract’s creation transaction.
Extract the input calldata.
Decode it according to:
constructor signature
ABI encoding rules
Read the “secret” directly.
This attack requires no interaction with the contract—just reading public blockchain data.
Resources
Last updated
Was this helpful?
