Secret Vault on Aptos

First Flight #46
Beginner FriendlyWallet
100 EXP
View results
Submission Details
Severity: high
Valid

Plaintext Secret Stored On-Chain (Confidentiality Broken)

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

    Answer: The vault should keep a user’s secret confidential so that only the owner can know its contents.


  • Explain the specific issue or problem in one or more sentences

    Answer: Move (and Aptos) state is public. Storing the secret as a String/vector in a has key resource means anyone can read it off-chain via a node/Explorer/CLI, regardless of function-level “auth checks.” Even with perfect access control in functions, the plaintext is still retrievable from global storage.

struct Vault has key {
@> secret: String
}

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

    Whenever the module publishes or updates a Vault, the plaintext is committed to global storage, which is readable by full nodes and RPCs.


  • Reason 2

    Whenever an observer queries the owner’s resources (e.g., explorer/CLI), the Vault.secret field can be inspected without calling any contract function.


Impact:

  • Impact 1

    Any third party can read the secret using standard state-read tools, defeating the purpose of a “secret vault.”


  • Impact 2

    Loss of user trust and potential leakage of sensitive data (e.g., API keys, passwords), rendering the app unsuitable for real-world secrets.

Proof of Concept

This test shows that the secret is readable by simply borrowing the global resource—no authentication step needed. Off-chain, the same is possible with explorers or CLI (e.g., querying the secret_vault::vault::Vault resource for the owner’s address).

#[test_only]
module secret_vault::poc_leak {
use secret_vault::vault;
use std::debug;
#[test(owner = @0xcc)]
fun test_plaintext_leak(owner: &signer) acquires vault::Vault {
// Owner stores a secret
let secret = b"my-plaintext-secret";
vault::set_secret(owner, secret);
// Any observer (including a test) can read the resource directly:
let v = borrow_global<vault::Vault>(@0xcc);
debug::print(&v.secret); // Prints the plaintext secret
}
}

Recommended Mitigation

nonReentrant: Prevents reentrancy exploits during withdrawals.

onlyAuthorized: Ensures only allowed roles (e.g., owner, whitelisted users, or those meeting criteria) can perform withdrawals.

This significantly reduces abuse risks and protects pooled funds.

- remove this code
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient funds");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
+ add this code
function withdraw(uint256 amount) public nonReentrant onlyAuthorized {
require(balances[msg.sender] >= amount, "Insufficient funds");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
Updates

Lead Judging Commences

bube Lead Judge 15 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Anyone can see the `secret` on chain

Support

FAQs

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