Secret Vault

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

Secret stored in Vault is publicly accessible from global storage breaking core invariant

Root + Impact

Description

The Vault resource is intended to store a secret that only the owner can access via the get_secret function. However, in Move on Aptos, all on-chain storage, which is a tree-shaped persistent global storage and programs cannot access the filesystem, network, or any other data outside of this tree, is publicly readable via full node RPC queries. This means that while Move enforces access control for contract calls, it does not prevent anyone from directly reading the underlying storage data. As a result, any party can retrieve the secret without calling get_secret.

// define type Vault
// field secret of type string
// key -> the struct can be stored in the global storage under an account address
// key allows struct to be used with: move_from, move_to, borrow_global
// contract ensures the secret is tied to the owner's address
struct Vault has key {
secret: String // utf-8 string
}

Risk

Likelihood:

  • Anyone with access to an Aptos full node or blockchain explorer can query account storage directly.

  • This applies immediately after the first call to set_secret and remains true as long as the resource exists.

Impact:

  • Complete exposure of the “secret” value to any network participant.

  • Violates the core functional requirement: “Only the owner should be able to store and retrieve the secret.”

Proof of Concept

The following PoC deploys the module locally and without calling any function, hits the REST API to show that the resource exists (or not) and can be read when created.

  • Start local Aptos node & faucet

aptos node run-local-testnet --with-faucet

Now we need to create a new terminal and:

After each step, if successful, the terminal will output a json with field "success": true

  • Initialize local account for deployment. It will ask you to enter a key, just press enter and it will generate account address and fund it

aptos init --profile 0x52 --network local
  • Publish/Deploy

aptos move publish --profile 0x52 --named-addresses owner=0x52,secret_vault={generated localnet owner address}
  • Set some random secret

aptos move run --function-id {generated localnet owner address}::vault::set_secret --args 'string:my_secret' --profile 0x52
  • Query resource via REST API without using the getter function.

The result should be a json including the secret value.

curl http://127.0.0.1:8080/v1/accounts/<owner_address>/resource/<owner_address>::vault::Vault
curl -s http://127.0.0.1:8080/v1/accounts/0x41e28042b2c616fa84c6d490a7ae956e9702348f8d58915b35dcf3491da72431/resource/0x41e28042b2c616fa84c6d490a7ae956e9702348f8d58915b35dcf3491da72431::vault::Vault | jq

Result:

{
"type": "0x41e28042b2c616fa84c6d490a7ae956e9702348f8d58915b35dcf3491da72431::vault::Vault",
"data": {
"secret": "my_secret"
}
}

Recommended Mitigation

Since true secrecy is impossible on-chain in Move (or any public blockchain), the only way to protect the secret is to store it encrypted where encrypt_offchain represents encryption performed before sending data on-chain, with the key managed off-chain.

public entry fun set_secret(caller:&signer,secret:vector<u8>){
- let secret_vault = Vault{secret: string::utf8(secret)};
+ let secret_vault = Vault{secret: encrypt_offchain(secret)};
move_to(caller,secret_vault);
event::emit(SetNewSecret {});
}

Alternatively, redesign the application so that sensitive information is never stored in plaintext on-chain.

Updates

Lead Judging Commences

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