Secret Vault on Aptos

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

Plaintext Storage of Sensitive Secrets in Vault Resource


Root + Impact

Description

  • The normal behavior expected in this module is for users to securely store a secret on the Aptos blockchain such that the secret is only accessible to the owner and remains confidential.

  • The specific issue is that the secret is stored as a UTF-8 encoded string in the Vault resource without any encryption or obfuscation. Since Aptos is a public blockchain, all on-chain data, including the secret stored as a plain string, is publicly visible to anyone who queries the blockchain state. This exposes the secret, violating confidentiality.

public entry fun set_secret(caller:&signer, secret:vector<u8>) {
let secret_vault = Vault{secret: string::utf8(secret)}; // @> secret stored as plain UTF-8 string
move_to(caller, secret_vault);
event::emit(SetNewSecret {});
}

Risk

Likelihood:

  • This occurs every time a user calls set_secret and stores a secret on-chain without encryption.

  • It applies universally to all users of the contract because Aptos stores data transparently across all nodes.

Impact:

  • Confidential secrets intended to be private are fully exposed and readable by any observer.

  • This can lead to theft of sensitive information, loss of user trust, and compromise of the intended security model of the contract.

Proof of Concept

The secret is stored as a String constructed from string::utf8(secret_bytes), i.e., plaintext. Any reader with chain state access (full nodes, indexers) can recover the exact text.

#[test(owner = @0xcc, bob = @0x456)]
fun test_plaintext_secret_is_leaked(owner: &signer, bob: &signer) acquires Vault {
use aptos_framework::account;
use std::string;
// 1) Create accounts
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(bob));
// 2) Owner stores a "secret" (actually plaintext)
let secret_bytes = b"MySuperSecret";
set_secret(owner, secret_bytes);
// 3) Anyone can read the resource at owner's address
let owner_addr = signer::address_of(owner);
let vault_ref = borrow_global<Vault>(owner_addr);
let expected = string::utf8(secret_bytes);
// 4) Assert: stored value equals original plaintext
assert!(vault_ref.secret == expected, 9001);
}
// Public view that leaks the plaintext
#[view]
public fun leak_plaintext(owner: address): String acquires Vault {
let v = borrow_global_mut<Vault>(owner);
// Move the string out of the resource instead of cloning
v.secret
}
#[test(owner = @0xcc)]
fun test_plaintext_view_leak(owner: &signer) acquires Vault {
use aptos_framework::account;
use std::string;
account::create_account_for_test(signer::address_of(owner));
set_secret(owner, b"TopSecret123");
let leaked = leak_plaintext(signer::address_of(owner));
assert!(leaked == string::utf8(b"TopSecret123"), 9002);
}

Recommended Mitigation

- let secret_vault = Vault{secret: string::utf8(secret)};
+ // Encrypt secrets off-chain before calling set_secret
+ // Store the encrypted bytes as a vector<u8> or a base64 string
+ let encrypted_secret = /* perform off-chain encryption */;
+ let secret_vault = Vault{secret: string::utf8(encrypted_secret)};
  • Document clearly the public nature of on-chain data to users and developers to avoid accidental exposure of sensitive information.

  • Alternatively, redesign the contract to store only encrypted secrets and add appropriate access controls to restrict reading.

Updates

Lead Judging Commences

bube Lead Judge about 2 months 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.