Secret Vault

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

Hardcoded @owner Address Mismatch Causing Incorrect or Exposed Secret Retrieval in get_secret

Root + Impact

Description

  • The get_secret function is intended to retrieve a stored secret by verifying the provided caller address against @owner and borrowing the Vault resource from that address, ensuring only the owner accesses their data. It complements set_secret, which stores the Vault under the caller's address, relying on Move's global storage for ownership enforcement.

  • The function hardcodes the borrow_global to @owner instead of using the caller parameter or signer's address, creating a mismatch where it only retrieves from the deployer's fixed address, not dynamically from where set_secret stores it. This renders secrets stored under other addresses inaccessible, while potentially exposing the deployer's secret, violating ownership invariants and the contract's single-owner intent.

#[view]
public fun get_secret (caller: address):String acquires Vault{
assert! (caller == @owner,NOT_OWNER);
// @> Root Cause Start: Hardcoded borrow from @owner, ignoring the caller parameter or dynamic storage from set_secret.
let vault = borrow_global<Vault >(@owner); // @> Always reads from fixed @owner, not caller.
vault.secret
// @> Root Cause End: Mismatch with set_secret's storage under caller, leading to incorrect retrieval or exposure.
}

Risk

Likelihood:

  • Deployers or users store secrets under addresses other than @owner via set_secret, but get_secret fails to retrieve them due to the hardcoded borrow.

  • The fixed @owner (from Move.toml or deployment) remains constant and discoverable, enabling targeted exploits when combined with the function's public access.

Impact:

  • Secrets stored under non-@owner addresses become irretrievable via get_secret, as it always borrows from the fixed @owner, leading to access failures and data loss for users.

  • If a secret is stored under @owner (e.g., by the deployer), it gets exposed to unauthorized callers due to the parameter bypass, amplifying privacy breaches.

Proof of Concept

This POC shows the mismatch: a secret is stored under a non-@owner address, but get_secret fails to retrieve it because it hardcodes to @owner. The test asserts that direct borrowing works (proving storage), but get_secret returns nothing or aborts, highlighting the flaw.

#[test(owner = @0xcc, user = @0x123)]
fun test_storage_under_different_addresses(owner: &signer, user: &signer) acquires Vault {
// 1. SETUP: Create accounts for owner (@owner) and a separate user
use aptos_framework::account;
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(user));
// 2. STORE SECRETS: Set under owner (@owner) and user addresses
let owner_secret = b"owner's secret";
set_secret(owner, owner_secret);
let owner_addr = signer::address_of(owner);
let user_secret = b"user's secret";
set_secret(user, user_secret);
let user_addr = signer::address_of(user);
// 3. VERIFY STORAGE: Direct borrows confirm both secrets are stored correctly
let owner_vault = borrow_global<Vault>(owner_addr);
assert!(owner_vault.secret == string::utf8(owner_secret), 6);
let user_vault = borrow_global<Vault>(user_addr);
assert!(user_vault.secret == string::utf8(user_secret), 7); // Proves storage works under user_addr
// NOTE: get_secret(owner_addr) would succeed (borrows from @owner), but get_secret(user_addr) would abort at assert
// because user_addr != @owner, and even if it passed, it would borrow from wrong address (@owner, not user_addr)
// 4. Debug print to confirm storage verification
debug::print(&b"--> POC Part 1: Secrets stored under different addresses, but retrieval mismatched for non-owner! <--");
}
#[test(owner = @0xcc, user = @0x123)]
#[expected_failure(abort_code = 1)] // Expects NOT_OWNER abort (code 1) when caller != @owner
fun test_get_secret_aborts_for_non_owner(owner: &signer, user: &signer) acquires Vault {
// SETUP: Create accounts and store a secret under user address
use aptos_framework::account;
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(user));
let user_secret = b"user's secret";
set_secret(user, user_secret);
let user_addr = signer::address_of(user);
// ATTEMPT RETRIEVAL: Calls get_secret with user_addr, which aborts at assert (user_addr != @owner)
let _ = get_secret(user_addr); // Aborts here, proving non-@owner secrets are inaccessible
// Debug print (won't reach due to abort)
debug::print(&b"--> POC Part 2: Should not reach here! <--");
}

Recommended Mitigation

Dynamically borrow from the caller's address (after verifying it) instead of hardcoding @owner. Combine with signer checks for security (as in earlier mitigations).

- #[view]
- public fun get_secret (caller: address):String acquires Vault{
- assert! (caller == @owner,NOT_OWNER);
- let vault = borrow_global<Vault >(@owner);
-
- vault.secret
- }
+ public entry fun get_secret(caller: &signer) acquires Vault {
+ let addr = signer::address_of(caller);
+ assert!(addr == @owner, NOT_OWNER); // Retain owner check if single-owner intended
+ let vault = borrow_global<Vault>(addr); // Dynamically borrow from caller's address
+ event::emit(GetSecret { secret: vault.secret }); // Emit instead of return (for entry functions)
+ }
Updates

Lead Judging Commences

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

The protocol doesn't work as intended

Support

FAQs

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