Secret Vault on Aptos

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

Anyone can retrieve the secret of the owner due to hardcoded owner address

Anyone can retrieve the secret of the owner due to hardcoded owner address

Description

  • The intended behavior is that only the contract owner (@owner) should be able to access their stored secret in the Vault.

  • The issue is that the function get_secret accepts an arbitrary address parameter and only checks whether it matches the hardcoded @owner. Since the owner address is publicly defined in Move.toml, anyone can supply this value and retrieve the secret. This makes the access control ineffective.

@>public fun get_secret(caller: address): String acquires Vault {
@> assert!(caller == @owner, NOT_OWNER);
let vault = borrow_global<Vault>(@owner);
vault.secret
}

Risk

Likelihood: High

  • The owner address is publicly visible in Move.toml and therefore easy to guess or obtain.

  • Any external caller can bypass the access control by passing caller = @owner.

Impact: High

  • Confidential data stored in the Vault is leaked to unauthorized users.

  • This compromises both confidentiality and trust in the contract, as secrets intended for the owner are fully exposed.

Proof of Concept

Add this Proof of concept to the secret_vault.move and run aptos move test.

#[test(user = @0x123, owner = @0xcc)]
fun attack(user: &signer, owner: &signer) acquires Vault {
use aptos_framework::account;
// Set up test environment
account::create_account_for_test(signer::address_of(user));
account::create_account_for_test(signer::address_of(owner));
// Create a new todo list for the user
let secret = b"i'm a secret";
set_secret(owner,secret);
// Attacker tries to read the owner's secret
let leaked_secret = secret_vault::vault::get_secret(@0xcc);
debug::print(&leaked_secret);
}

The attacker successfully retrieves the owner’s secret by supplying the known hardcoded address.

Recommended Mitigation

The recommended mitigation changes the function to accept a &signer reference instead of a raw address. This allows the function to dynamically obtain the caller’s actual address at runtime using signer::address_of(caller). By asserting against this runtime address and borrowing the resource for that specific signer, the contract enforces proper access control and ensures that only the true owner can read their secret.

- public fun get_secret(caller: address): String acquires Vault {
- assert!(caller == @owner, NOT_OWNER);
- let vault = borrow_global<Vault>(@owner);
- vault.secret
- }
+ public fun get_secret(caller: &signer): String acquires Vault {
+ let caller_address = signer::address_of(caller);
+ assert!(caller_address == @owner, NOT_OWNER);
+ let vault = borrow_global<Vault>(caller_address);
+ vault.secret
+ }
Updates

Lead Judging Commences

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