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:
The function accepts an owner: &signer parameter that can be provided by any caller.
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.
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.
Attackers can drain the airdrop pool by registering multiple addresses
Unauthorized access: Bypasses intended access control mechanisms
Resource exhaustion: Can register unlimited addresses, consuming contract storage
Undermines trust in the airdrop system
Proof of Concept
Attacker knows the contract owner's address (publicly available on-chain) and has sufficient gas to execute transactions
Airdrop pool has funds available
// Owner's address: 0xOWNER (stored in state.owner)
transaction {
public entry fun exploit_register_pizza_lover() {
let owner_addr = @0xOWNER;
// Create a signer reference to pass as the owner parameter
let fake_owner_signer = account::create_signer(owner_addr);
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,
});
}