SNARKeling Treasure Hunt

First Flight #59
Beginner FriendlyGameFiFoundry
100 EXP
Submission Details
Impact: high
Likelihood: high

Plaintext private key exposure in deployment script, anyone can access the private key of the contract

Author Revealed upon completion

Plaintext private key exposure in deployment script, anyone can access the private key of the contract

Description

  • The deployment script utilizes vm.envUint("PRIVATE_KEY") to load the deployer's cryptographic credentials directly from the local environment. This pattern typically implies that the private key is stored in a .env file or exported as an environment variable in the terminal.

  • Because these files and shell histories are stored in plaintext, the most sensitive piece of information in the protocol (The private Key) is left unencrypted on the disk.

// Root cause in the codebase with @> marks to highlight the relevant section
contract Deploy is Script {
uint256 constant DEFAULT_INITIAL_FUNDING = 100 ether;
HonkVerifier verifier;
TreasureHunt hunt;
function run() external {
//@> uint256 deployerKey = vm.envUint("PRIVATE_KEY");
uint256 initialFunding = vm.envOr("INITIAL_FUNDING", DEFAULT_INITIAL_FUNDING);
address deployer = vm.addr(deployerKey);
console2.log("Chain ID:", block.chainid);
console2.log("Deployer:", deployer);
console2.log("Deployer balance:", deployer.balance);
console2.log("Initial funding:", initialFunding);
vm.startBroadcast(deployerKey);
verifier = new HonkVerifier();
hunt = new TreasureHunt{value: initialFunding}(address(verifier));
vm.stopBroadcast();
vm.label(address(verifier), "HonkVerifier");
vm.label(address(hunt), "TreasureHunt");
require(address(verifier) != address(0), "INVALID_VERIFIER_DEPLOY");
require(address(hunt) != address(0), "INVALID_HUNT_DEPLOY");
require(hunt.getContractBalance() == initialFunding, "UNEXPECTED_BALANCE");
console2.log("Verifier:", address(verifier));
console2.log("TreasureHunt:", address(hunt));
console2.log("Balance:", hunt.getContractBalance());
console2.log("Reward:", hunt.REWARD());
console2.log("Max treasures:", hunt.MAX_TREASURES());
console2.log("Remaining treasures:", hunt.getRemainingTreasures());
console2.log("Paused:", hunt.isPaused());
}

Risk

Likelihood:

  • The likelihood is high because developers frequently commit .env files to version control (GitHub) by mistake, or leave them on shared development machines.

  • CI/CD pipelines (like GitHub Actions) log environment variables by default, potentially exposing the key in build logs accessible to the entire organization.

Impact:

  • An attacker can drain the initialFunding (100 ETH) before the script even finishes.

  • The attacker can become the owner of the TreasureHunt contract, allowing them to withdraw future funds or disrupt the game.

Proof of Concept

This test proves that if a private key is loaded into the environment, it is "visible" to the execution context, making it easy for malicious scripts or accidental logs to expose it.

// forge test --mt test_PoC_PrivateKeyLeak -vv
function test_PoC_PrivateKeyLeak() public {
// 1. SETUP: Simulate a developer having their key in a .env file
// We set the env var manually here to simulate the .env being loaded
vm.setEnv("PRIVATE_KEY", "1234567890123456789012345678901234567890123456789012345678901234");
// 2. THE EXPLOIT: Malicious code or a simple script can read this directly
// Any imported library or tool could execute this line:
uint256 leakedKey = vm.envUint("PRIVATE_KEY");
// 3. LOGGING: Demonstrating how easy it is to accidentally log it
// This simulates the key appearing in CI/CD logs or terminal history
console2.log("--- CRITICAL LEAK DETECTED ---");
console2.log("Private Key found in plaintext:", leakedKey);
// 4. VERIFICATION: Prove that the leaked key is the actual sensitive key
address deployer = vm.addr(leakedKey);
console2.log("This key controls address:", deployer);
assertEq(leakedKey, 1234567890123456789012345678901234567890123456789012345678901234);
}

Recommended Mitigation

To mitigate the Private Key Exposure issue, the goal is to remove the private key from the Solidity code and environment variables entirely.

The script should only focus on the deployment logic.

-All the Old -contract Deploy is Script {
+contract Deploy is Script {
+ function run() external {
+ // NOTICE: No private keys are loaded here. // No vm.envUint("PRIVATE_KEY") exists.
+ uint256 initialFunding = vm.envOr("INITIAL_FUNDING", 100 ether);
+ // This starts the recording of transactions.
+ // The account used is determined by the CLI flag --account, not code.
+ vm.startBroadcast();
+ HonkVerifier verifier = new HonkVerifier();
+ new TreasureHunt{value: initialFunding}(address(verifier));
+ vm.stopBroadcast();
+ }
+}

Support

FAQs

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

Give us feedback!