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:

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 c

Mappings 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 i

Strings 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

Resources

Last updated

Was this helpful?