Unchecked from address in L1 Handler function
A function with the l1_handler
annotation is intended to be called from L1. The first parameter of the l1_handler
function is always from
, which represents the msg.sender
of the L1 transaction that attempted to invoke the function on Starknet. If the l1_handler
function is designed to be invoked from a specific address on mainnet, not checking the from address may allow anyone to call the function, opening up access control vulnerabilities.
Example
The following Starknet bridge contract's owner, specified in the uint256[] calldata payload
array, is designed to be called only from the setOwnerOnL2()
function. Even though the owner is checked on the solidity side, the lack of validation of the from_address
parameter allows anyone to call the function from an arbitrary L1 contract, becoming the owner of the bridge on L2.
address public immutable OWNER;
address public immutable MESSENGER_CONTRACT;
address public immutable L2_BRIDGE_ADDRESS;
constructor(address _owner, address _messenger, address _bridge) {
OWNER = _owner;
MESSENGER_CONTRACT = _messenger;
L2_BRIDGE_ADDRESS = _bridge;
}
function setOwnerOnL2(uint256[] calldata payload, uint256 selector) external {
require(owner == msg.sender, "not owner");
IStarknetMessaging(MESSENGER_CONTRACT).sendMessageToL2(L2_BRIDGE_ADDRESS, selector, payload);
}
#[storage]
struct Storage {
owner: ContractAddress
}
#[l1_handler]
fn set_owner_from_l1(ref self: ContractState, from_address: felt252, new_owner: ContractAddress) {
self.owner.write(new_owner);
}
Mitigations
- Make sure to validate the
from_address
, otherwise any L1 contract can invoke the annotated Starknet function. - Consider using Caracal, as it comes with a detector for verifying if the
from_address
is unchecked in anl1_handler
function.