Secret Vault on Aptos

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

H01. Secret can be easily retrieve in the transaction payload through explorer

Root + Impact

Description

Normal behavior:
The module secret_vault::vault lets a user call set_secret(&signer, secret: vector<u8>) to store a String secret in a Vault resource under their account and later read it via get_secret(address).

Specific issue:
The secret is supplied as a transaction argument and converted to a String on-chain. On Aptos, transaction payloads (including arguments) are public and trivially viewable in block explorers and full node APIs. This means the secret is exposed even before it’s written to state, and storing it on-chain preserves the plaintext forever.

// Root cause in the codebase with @> marks to highlight the relevant section
module secret_vault::vault {
use std::string::{Self, String};
struct Vault has key { secret: String }
public entry fun set_secret(caller: &signer, secret: vector<u8>) {
// @> 'secret' is a transaction argument (public in payload/explorer)
// @> converting and persisting it guarantees permanent plaintext exposure
let secret_vault = Vault { secret: string::utf8(secret) };
move_to(caller, secret_vault); // @> persists plaintext on-chain
// (event omitted)
}
#[view]
public fun get_secret(caller: address): String acquires Vault {
// (separate auth bug aside) the confidentiality is already lost at payload time
let vault = borrow_global<Vault>(caller);
vault.secret
}
}

Risk

Likelihood: High

  • Every invocation of set_secret necessarily includes the plaintext secret as a transaction argument; this occurs on all successful and reverted transactions alike because payloads are broadcast.

  • Block explorers and public RPCs routinely index and display transaction arguments, making retrieval trivial for anyone.

Impact: Severe

  • Immediate and irreversible disclosure of sensitive information (passwords, API keys, PII) to the public.

  • Regulatory/compliance exposure and downstream account compromise wherever the secret is reused.

Proof of Concept

1) Deploy the program on the dev network

aptos move publish --profile local --dev

2) Call the function to set the secret with your secret, here hello!


aptos move run --function-id 0xf9228a020889c982c81387041ed2c9a28b7560752798dd9738795ab00666f4a6::secret_vault::set_secret --args string:hello!


3)
Go the explorer and check the payload of the transaction
https://explorer.aptoslabs.com/txn/0x7ae7e3c42c07f0a487a7feaa56a6db5d97e8106568d2bb0938f0c42d968a0aa7/payload?network=local
You will have the secret in hex in the argument:

arguments:[0:
"0x68656c6c6f21"]

68656c6c6f21 ==> hello! in hexa\

Recommended Mitigation

  • Do not store secret on-chain.
    If required: at least encrypt them before storing them or only commit the hash if only use for comparison

- public entry fun set_secret(caller: &signer, secret: vector<u8>) {
- let secret_vault = Vault { secret: string::utf8(secret) };
- move_to(caller, secret_vault);
- event::emit(SetNewSecret {});
- }
+ /// DO NOT send plaintext secrets on-chain.
+ /// Option A: store only a non-reversible commitment (e.g., hash) for comparison.
+ public entry fun set_secret_commitment(caller: &signer, secret_hash: vector<u8>) {
+ // secret_hash = Keccak/SHA-3/Blake2 hash computed OFF-CHAIN
+ let vault = Vault { secret: string::utf8(secret_hash) };
+ move_to(caller, vault);
+ }
+
+ /// Option B: if storage is required, accept ONLY ciphertext produced OFF-CHAIN.
+ /// Encrypt client-side with the reader’s public key; store opaque bytes.
+ struct EncVault has key { ciphertext: vector<u8> }
+ public entry fun set_secret_ciphertext(caller: &signer, ciphertext: vector<u8>) {
+ // ciphertext must be produced off-chain (e.g., X25519/ECIES + AEAD)
+ let v = EncVault { ciphertext };
+ move_to(caller, v);
+ }
+
+ /// Additional hardening:
+ /// - Never log or event-emit plaintext or derived values that leak info.

Design guidance:

  • Never transmit plaintext secrets in transaction arguments. Do not rely on access control to keep data private; on public chains, privacy is not provided by default.

  • Use client-side encryption (hybrid/public-key) and store only ciphertext, or store a one-way commitment (hash/salt/nonce) if you only need verification.

  • Consider eliminating on-chain storage entirely and using an off-chain secret manager, with the chain storing only references or commitments.

Updates

Lead Judging Commences

bube Lead Judge 17 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.