Secret Vault on Aptos

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

Unauthorized Secret Disclosure via Broken Authentication in get_secret

Root + Impact

Description

  • Intended behavior: Only the owner is allowed to store a secret and later retrieve it. No other account should be able to access the secret.

  • Actual behavior (bug): The get_secret function authorizes by comparing a user-supplied address to the named address @owner instead of authenticating the actual caller (no &signer is used). As a result, any account can pass the owner’s address as the argument and read the owner’s secret.

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

Why this is exploitable:
Authentication is based on what argument the caller supplies, not on who is calling. Because the owner’s address is publicly knowable after deployment (named addresses are bound into published bytecode and visible on-chain), any user can call get_secret(owner_addr) and obtain the secret.

Risk

Likelihood:

  • The function is public #[view] and callable by any account; no privilege is required.

  • The owner’s on-chain address is observable after deployment (e.g. via explorer), so the required parameter value is trivially discoverable.

Impact:

  • Direct disclosure of the owner’s secret, violating the core requirement (“Only the owner should be able to store a secret and then retrieve it later”).

  • Irreversible data breach: once read, the secret cannot be “unseen”; redeploying does not mitigate prior leakage.

Proof of Concept

The unit test creates two accounts (owner and attacker, the attacker parameter is present only to emphasize roles; it’s not required for the unit test to pass).

The owner stores a secret via set_secret. The exploit then calls get_secret(owner_addr) and succeeds because the function authorizes by the user-supplied address parameter instead of the actual caller (&signer).

The final assert verifies that the value returned by get_secret equals the original secret, confirming an unauthorized read by any caller who passes owner_addr.

#[test(owner = @0xcc, attacker = @0x123)]
fun test_leak_owners_secret(owner: &signer, attacker: &signer) acquires Vault {
use aptos_framework::account;
// Prepare account(s)
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(attacker)); // not necessary
// Owner stores a secret
let s = b"i'm a secret";
set_secret(owner, s);
// Save owner`s address
let owner_addr = signer::address_of(owner);
// Exploit: anyone can pass `owner_addr` to the view and read the secret
let leaked = get_secret(owner_addr);
// Verify leakage
assert!(leaked == string::utf8(s), 100);
}

Recommended Mitigation

Authenticate the actual caller via &signer (and keep storage/reads bound to @owner).

- 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 {
+ // Authorize by signer, not by user-supplied address
+ assert!(signer::address_of(caller) == @owner, NOT_OWNER);
+ let v = borrow_global<Vault>(@owner);
+ // Return a copy of the secret; use your project’s preferred string copy helper
+ string::clone(&v.secret)
+ }
Updates

Lead Judging Commences

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