Secret Vault

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

get_secret authorization bypass allows arbitrary reads of owner’s secret

Description

The current implementation of #[view] get_secret(address) does not authenticate the caller. Instead, it takes an address argument and asserts that it equals the hardcoded @owner. Any user can simply pass @owner as the parameter to the function and obtain the owner’s secret. This creates a direct logic-level authorization bypass.

#[view]
public fun get_secret(caller: address): String acquires Vault {
assert!(caller == @owner, NOT_OWNER); // checks argument, not signer
let vault = borrow_global<Vault>(@owner);
vault.secret
}

Root Cause

  • The function signature accepts a free address parameter instead of a &signer, so it has no knowledge of who is calling it.

  • The check assert!(caller == @owner, NOT_OWNER) validates only that the argument equals @owner, not that the transaction signer is the owner.

  • Returning the stored String directly exposes the secret to any caller who passes @owner.


Risk

Likelihood:

  • Every call to get_secret only checks the argument, not the actual caller. This means the bypass will occur every time an attacker supplies @owner as input.

  • Any user with knowledge of the owner address (trivial to obtain from blockchain data) can perform this call without restriction.

Impact:

  • Any attacker can retrieve the owner’s secret without being authorized.

  • This invalidates the “only the owner may retrieve” guarantee in the project specification.

  • When combined with the design-level confidentiality flaw, it means that both off-chain and on-chain access controls fail.

Proof of Concept

#[test(owner = @0xcc, user = @0x123)]
fun test_anyone_can_read_owner_secret(owner: &signer, user: &signer) acquires Vault {
// owner sets secret
let secret = b"not-so-secret";
set_secret(owner, secret);
// non-owner calls get_secret(@owner)
let read = get_secret(@owner);
// passes and leaks secret
assert!(read == string::utf8(secret), 100);
}

This test demonstrates that a non-owner account (@0x123) can read the owner’s secret.

Recommended Mitigation

  • Replace the address parameter with an authenticated signer:

- #[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): String acquires Vault {
+ let addr = signer::address_of(caller);
+ assert!(addr == @owner, NOT_OWNER);
+ let vault = borrow_global<Vault>(addr);
+ string::clone(&vault.secret)
+ }
  • Alternatively, if #[view] is required:

    • Do not expose sensitive plaintext in a view function, as these bypass signer authentication by design.

    • Instead, expose only non-sensitive metadata (e.g., “secret exists” flag, hash, or ciphertext length).

Updates

Lead Judging Commences

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