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 16 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.