Secret Vault on Aptos

First Flight #46
Beginner FriendlyWallet
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Complete Access Control Bypass Allows Unauthorized Secret Storage and Prevents Legitimate Access

Root + Impact

Description

  • The SecretVault contract is designed to allow only the owner to store and retrieve secrets, ensuring exclusive access control to sensitive data stored on-chain.

  • The contract implements inconsistent access control between the set_secret and get_secret functions, where any user can store secrets but only the hardcoded @owner address can retrieve them, completely breaking the intended security model and making secrets inaccessible to legitimate users.

More info about this can be accessed through : https://github.com/CodeHawks-Contests/2025-07-secret-vault/pull/2

public entry fun set_secret(caller:&signer,secret:vector<u8>){
let secret_vault = Vault{secret: string::utf8(secret)};
move_to(caller,secret_vault); // ❌ No access control - any caller can store
event::emit(SetNewSecret {});
}
#[view]
public fun get_secret (caller: address):String acquires Vault{
assert! (caller == @owner,NOT_OWNER); // ❌ Only hardcoded @owner can retrieve
let vault = borrow_global<Vault >(@owner); // ❌ Always reads from @owner account
vault.secret
}

Risk

Likelihood:

  • Reason 1 Any user can call the public set_secret function without restrictions, as there are no access control checks preventing unauthorized secret storage

  • Reason 2 The vulnerability is present in every transaction involving secret management, making it inevitable that unauthorized users will interact with the contract

Impact:

  • Impact 1 Legitimate users (non-owners) who store secrets will permanently lose access to their data, as only the hardcoded @owner can retrieve secrets

  • Impact 2 The contract fails its primary security objective of providing exclusive secret storage, rendering it completely unusable for its intended purpose


Proof of Concept

This proof of concept demonstrates how the access control bypass vulnerability allows any user to store secrets while preventing legitimate access to those secrets. The test simulates a real-world scenario where a user successfully stores confidential data but then discovers it's permanently inaccessible due to the flawed access control logic.

The vulnerability occurs because set_secret accepts any signer without validation, while get_secret only allows the hardcoded @owner address and always reads from the @owner account regardless of where the secret was actually stored. This creates a critical disconnect between storage and retrieval operations.

#[test(owner = @0xcc, user = @0x123)]
fun test_access_control_bypass_poc(owner: &signer, user: &signer) acquires Vault {
use aptos_framework::account;
// Setup test accounts
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(user));
// Step 1: User successfully stores a secret (should fail but doesn't)
let user_secret = b"user's confidential data";
set_secret(user, user_secret); // ✅ This succeeds - VULNERABILITY
// Step 2: User cannot retrieve their own secret
let user_addr = signer::address_of(user);
// get_secret(user_addr); // ❌ This would fail with NOT_OWNER error
// Step 3: Owner cannot access user's secret either
// get_secret(@owner); // ❌ This would fail - no Vault resource at @owner
// Result: Secret is permanently inaccessible to everyone
assert!(exists<Vault>(user_addr), 1); // User's vault exists
// But no one can retrieve the secret due to flawed access logic
}

Recommended Mitigation

The mitigation strategy addresses the root cause by implementing consistent access control across both functions and fixing the resource management logic. The solution ensures that only the authorized owner can store secrets and that the retrieval logic correctly accesses the owner's account where the secret is stored.

Key improvements include: (1) Adding proper access control validation in set_secret, (2) Implementing proper resource update handling for existing vaults, (3) Aligning the storage and retrieval logic to use consistent account addressing, and (4) Adding comprehensive error handling for edge cases.

public entry fun set_secret(caller: &signer, secret: vector<u8>) {
// Add proper access control check
assert!(signer::address_of(caller) == @owner, NOT_OWNER);
let caller_addr = signer::address_of(caller);
// Handle existing vault properly
if (exists<Vault>(caller_addr)) {
let vault = borrow_global_mut<Vault>(caller_addr);
vault.secret = string::utf8(secret);
} else {
let secret_vault = Vault { secret: string::utf8(secret) };
move_to(caller, secret_vault);
};
event::emit(SetNewSecret {});
}
#[view]
public fun get_secret(caller: address): String acquires Vault {
assert!(caller == @owner, NOT_OWNER);
// Read from the caller's account, not hardcoded @owner
let vault = borrow_global<Vault>(caller);
vault.secret
}
Updates

Lead Judging Commences

bube Lead Judge 16 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

Anyone can call `set_secret` function

In Move for Aptos, the term "owner" refers to a signer, which is a verified account that owns a given resource, has permission to add resources and the ability to grant access or modify digital assets. Following this logic in this contest, the owner is the account that owns `Vault`. This means that anyone has right to call `set_secret` and then to own the `Vault` and to retrieve the secret from the `Vault` in `get_secret` function. Therefore, this group is invalid, because the expected behavior is anyone to call the `set_secret` function.

Support

FAQs

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