Secret Vault on Aptos

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

Critical Access Control Bypass in get_secret Function of Vault Module

Root + Impact

Description

  • Normal behavior:
    The get_secret function should only allow the rightful owner of a Vault resource to retrieve their own secret, while preventing access by unauthorized accounts.


  • Specific issue:
    The function hardcodes @owner as the retrieval address, but set_secret stores the Vault resource under the caller’s address. This mismatch means:

    • Non-owners cannot retrieve their own secret (because their data is never read).

    • Any account can pass @owner into the get_secret parameter to retrieve the owner’s secret without authorization.
      Additionally, the caller parameter in get_secret is not tied to a signer, so the assert!(caller == @owner, NOT_OWNER); check is meaningless.

// Root cause in the codebase
#[view]
public fun get_secret (caller: address): String acquires Vault {
assert!(caller == @>owner<, NOT_OWNER);
let vault = borrow_global<Vault>(@>owner<);
vault.secret
}

Risk

Likelihood:

  • Always reproducible — any account can retrieve the @owner's secret by calling get_secret(@owner).

  • Non-owners will always fail to retrieve their own secrets due to the hardcoded storage address.

Impact:

  • Confidential information in the owner’s Vault is exposed to all accounts.

  • Legitimate users are denied access to their own data.

  • Access control enforcement is completely bypassed.

Proof of Concept

This test demonstrates that an arbitrary account (bob) can bypass access control by simply passing the hardcoded @owner value into get_secret. This allows them to retrieve sensitive data without owning the Vault. The test passes, proving the vulnerability.

#[test(owner = @0xcc, bob = @0x456)]
fun test_bypass_with_owner_address(owner: &signer, bob: &signer) acquires Vault {
use aptos_framework::account;
use std::string;
// Create accounts
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(bob));
// Owner sets secret
set_secret(owner, b"owner-secret");
// Bob retrieves the owner's secret by passing caller = @owner
let stolen = get_secret(@owner);
assert!(stolen == string::utf8(b"owner-secret"), 200);
}
// Ouput
[ PASS ] 0x234::vault::test_bypass_with_owner_address
Test result: OK. Total tests: 1; passed: 1; failed: 0

Recommended Mitigation

The fix ensures that the function reads from the address provided by the signer, not a hardcoded named address. This prevents arbitrary accounts from retrieving another account’s secret.


If you want absolute access control, tie caller to a &signer argument, so only the transaction signer can retrieve their own secret. Alternatively, store an owner: address field inside Vault and verify it against the signer’s address.

- assert!(caller == @owner, NOT_OWNER);
- let vault = borrow_global<Vault>(@owner);
+ let vault = borrow_global<Vault>(caller);
+ // If only the signer should see their own secret:
+ public fun get_secret_for_signer(caller: &signer): String acquires Vault {
+ let addr = signer::address_of(caller);
+ let vault = borrow_global<Vault>(addr);
+ string::clone(&vault.secret)
+ }
Updates

Lead Judging Commences

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

Lack of signer check in `get_secret`

Support

FAQs

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