Beginner FriendlyGameFi
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Access Control Bypass Vulnerability

Access Control Bypass Vulnerability

Description

The register_pizza_lover function contains an access control bypass vulnerability that allows any user to register themselves or other addresses to receive airdrop funds. The function incorrectly validates the caller's identity by checking a parameter (owner) instead of verifying the actual caller's address.

public entry fun register_pizza_lover(owner: &signer, user: address) acquires ModuleData, State {
let state = borrow_global_mut<State>(get_resource_address());
@> assert!(signer::address_of(owner) == state.owner, E_NOT_OWNER);
get_random_slice(user);
event::emit(PizzaLoverRegistered {
user: user,
});
}

The vulnerability occurs because:

  1. The function accepts an owner: &signer parameter that can be provided by any caller.

  2. The access control check assert!(signer::address_of(owner) == state.owner, E_NOT_OWNER) only verifies that the provided owner parameter matches the stored owner address.

  3. It doesn't verify that the actual caller (&signer of the transaction) is the owner.

Risk

Likelihood:High - Any user can exploit this vulnerability.

Impact: Critical - Allows unauthorized registration for airdrop claims.

  • Impact

  1. Attackers can drain the airdrop pool by registering multiple addresses

  2. Unauthorized access: Bypasses intended access control mechanisms

  3. Resource exhaustion: Can register unlimited addresses, consuming contract storage

  4. Undermines trust in the airdrop system

Proof of Concept

  1. Attacker knows the contract owner's address (publicly available on-chain) and has sufficient gas to execute transactions

  2. Airdrop pool has funds available

// Malicious actor can call register_pizza_lover with the owner's address
// even though they are not the owner
// Attacker's address: 0xATTACKER
// Owner's address: 0xOWNER (stored in state.owner)
// Malicious transaction that bypasses access control
transaction {
public entry fun exploit_register_pizza_lover() {
// Get owner's address (could be obtained from on-chain data)
let owner_addr = @0xOWNER;
// Create a signer reference to pass as the owner parameter
// This is possible because the function doesn't verify the actual caller
let fake_owner_signer = account::create_signer(owner_addr);
// Call register_pizza_lover with attacker's address as user
pizza_drop::airdrop::register_pizza_lover(&fake_owner_signer, @0xATTACKER);
}
}

Recommended Mitigation

- public entry fun register_pizza_lover(owner: &signer, user: address) acquires ModuleData, State {
- let state = borrow_global_mut<State>(get_resource_address());
- assert!(signer::address_of(owner) == state.owner, E_NOT_OWNER);
- get_random_slice(user);
- event::emit(PizzaLoverRegistered {
- user: user,
- });
- }
+ public entry fun register_pizza_lover(caller: &signer, user: address) acquires ModuleData, State {
+ let state = borrow_global_mut<State>(get_resource_address());
+
+ // FIX: Verify the actual caller is the owner, not a parameter
+ assert!(signer::address_of(caller) == state.owner, E_NOT_OWNER);
+
+ get_random_slice(user);
+ event::emit(PizzaLoverRegistered {
+ user: user,
+ });
+ }

Also if we want we can remove owner parameter entirely if no used of it.

- public entry fun register_pizza_lover(owner: &signer, user: address) acquires ModuleData, State {
+ public entry fun register_pizza_lover(caller: &signer, user: address) acquires ModuleData, State {
let state = borrow_global_mut<State>(get_resource_address());
- assert!(signer::address_of(owner) == state.owner, E_NOT_OWNER);
+ assert!(signer::address_of(caller) == state.owner, E_NOT_OWNER);
get_random_slice(user);
event::emit(PizzaLoverRegistered {
user: user,
});
}
Updates

Appeal created

bube Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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