An access control vulnerability occurs when a system or application fails to properly restrict access to sensitive functions, data, or resources, allowing unauthorized users to perform actions they shouldn’t be permitted to do.
In the context of smart contracts (e.g., on Ethereum), this vulnerability often arises when critical functions (like withdrawing funds, updating contract state, or changing ownership) lack proper authorization checks, enabling anyone to call them.
This can lead to severe consequences, such as:
- Unauthorized users draining funds.
- Malicious changes to contract state or configuration.
- Loss of control over the contract.
Example: Vulnerable vs. Non-Vulnerable Smart Contract
Below are two Solidity smart contracts: one vulnerable to access control issues and one non-vulnerable with proper access control.
Vulnerable Contract
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VulnerableBank {
mapping(address => uint256) public balances;
address public owner;
constructor() {
owner = msg.sender;
}
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// Vulnerable: No access control on withdrawAll
function withdrawAll() public {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance to withdraw");
balances[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
// Vulnerable: No access control on changeOwner
function changeOwner(address newOwner) public {
owner = newOwner;
}
}
Issues in the Vulnerable Contract:
- The
withdrawAll function
allows any user to withdraw their balance without additional checks, which might be intended only for specific users (e.g., an admin or authorized account). - The
changeOwner function
can be called by anyone, allowing an attacker to take control of the contract by setting themselves as the owner. - No use of access control mechanisms like require or modifiers to restrict access.
Non-Vulnerable Contract
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SecureBank {
mapping(address => uint256) public balances;
address public owner;
// Modifier to restrict access to owner only
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function");
_;
}
constructor() {
owner = msg.sender;
}
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// Secure: Only owner can withdraw all funds
function withdrawAll() public onlyOwner {
uint256 totalBalance = address(this).balance;
require(totalBalance > 0, "No balance to withdraw");
payable(owner).transfer(totalBalance);
}
// Secure: Only owner can change ownership
function changeOwner(address newOwner) public onlyOwner {
require(newOwner != address(0), "Invalid address");
owner = newOwner;
}
}
Improvements in the Non-Vulnerable Contract:
- The
onlyOwner modifier
ensures that only the owner can call sensitive functions like withdrawAll and changeOwner. - The
require(msg.sender == owner, ...)
check enforces access control. - Additional validation (e.g., newOwner != address(0)) prevents invalid state changes.
- The withdrawAll function now withdraws the entire contract balance to the owner, aligning with a typical admin-controlled contract.
Things to Look For (Keywords, Patterns, and Red Flags)

To identify potential access control vulnerabilities, a novice should look for the following:
Keywords and Patterns:
- Missing require or if checks: Functions that modify critical state (e.g., ownership, balances, or contract settings) should include checks like
require(msg.sender == owner, ...)
or similar. - Absence of modifiers: Look for custom modifiers like onlyOwner, onlyAdmin, or restricted that enforce access control. If a contract lacks these for sensitive functions, it’s a red flag.
- Use of
msg.sender
without validation: If msg.sender is used to update state or authorize actions without verifying its legitimacy, the function may be vulnerable. - Public/external functions: Functions marked public or external that perform sensitive operations (e.g., withdrawing funds, changing ownership) should be scrutinized for access control.
Red Flags:
- No ownership check: Functions like withdraw, transfer, setOwner, or updateConfig that don’t verify the caller’s identity.
- Unrestricted state changes: Functions that allow anyone to modify critical variables (e.g., owner, balances, or contract settings).
- Missing events for critical actions: Secure contracts often emit events (e.g., event OwnershipTransferred(address oldOwner, address newOwner)) for transparency, which may be absent in vulnerable contracts.
- Hardcoded addresses without validation: If a function uses a hardcoded address or input address without checking its validity, it could be exploited.
Positive Indicators:
- Presence of modifiers like onlyOwner, onlyAdmin, or restricted.
- Use of require or assert to validate the caller (e.g., require(msg.sender == owner, “Unauthorized”)).
- Use of role-based access control libraries like OpenZeppelin’s Ownable or AccessControl for standardized, secure access management.
- Functions marked internal or private for operations that shouldn’t be called externally.
How a Novice Can Identify Missing Access Control
To determine if a contract lacks access control, a novice can follow these steps:
- Read the Function Visibility:
- Check if sensitive functions (e.g., those that withdraw funds, change ownership, or update state) are marked public or external. If they are, ensure there’s a mechanism to restrict access.
- Look for Authorization Checks:
- Search for require statements or modifiers that verify msg.sender against an owner, admin, or authorized role.
- Example: require(msg.sender == owner, “Not owner”) or a modifier like onlyOwner.
- Check for Modifiers:
- Look for custom modifiers (e.g., modifier onlyOwner) that enforce access control. If none exist, it’s a potential issue.
- Analyze State-Changing Functions:
- Identify functions that modify critical variables (e.g., owner, balances, or contract settings). If these lack restrictions, the contract is likely vulnerable.
- Use Tools:
- Run static analysis tools like Slither or Mythril to detect missing access control. These tools flag functions that lack proper authorization.
- Example Slither output: Function withdraw() is public and does not have access control.
- Compare with Standards:
- Compare the contract with secure templates, such as OpenZeppelin’s Ownable contract, which includes a standard onlyOwner modifier for access control.
- Test Manually:
- Deploy the contract on a testnet and try calling sensitive functions from a non-owner account. If the call succeeds, there’s an access control vulnerability.
Additional Tips for Newbies
- Learn from Libraries: Study OpenZeppelin’s Ownable and AccessControl contracts to understand best practices for access control.
- Audit Contracts: If reviewing code, use a checklist to ensure all sensitive functions have proper access control.
- Stay Updated: Follow security blogs (e.g., OpenZeppelin, ConsenSys) and learn from real-world exploits (e.g., Parity Wallet hack due to missing access control).
- Ask for Help: If unsure, consult communities like Ethereum Stack Exchange or Crypto Twitter for feedback on contract security.
By focusing on these patterns and practices, novices can better identify and prevent access control vulnerabilities in smart contracts.
Key Questions to Asks When Looking for Access Control Vulnerabilities
- Function Visibility and Accessibility:
- Are sensitive functions (e.g., those that withdraw funds, change ownership, or modify critical state) marked public or external?
- If a function is public or external, does it include checks to restrict who can call it?
- Are there any functions that should be internal or private but are unnecessarily exposed?
- Authorization Checks:
- Does the function verify the caller’s identity (e.g., msg.sender) against an owner, admin, or authorized role?
- Are there require or if statements to enforce access control (e.g., require(msg.sender == owner, “Unauthorized”))?
- Are there any sensitive functions that lack authorization checks entirely?
- Use of Modifiers:
- Does the contract use modifiers (e.g., onlyOwner, onlyAdmin) to enforce access control?
- Are modifiers applied consistently to all sensitive functions?
- Are the modifiers themselves secure (e.g., do they include proper checks and avoid logic errors)?
- Critical State Changes:
- Which functions modify critical state variables (e.g., owner, balances, contract settings)?
- Do these functions restrict access to authorized users only?
- Can unauthorized users manipulate critical state directly or indirectly?
- Ownership and Role Management:
- How is ownership or admin status assigned (e.g., in the constructor or via a function)?
- Can ownership or roles be changed? If so, is the changeOwner or similar function restricted to the current owner?
- Are there multiple roles (e.g., admin, minter, pauser)? If so, is access control enforced for each role?
- Input Validation:
- Do functions that accept addresses (e.g., for ownership transfer or fund withdrawal) validate that the address is not address(0) or invalid?
- Are there checks to prevent unauthorized users from passing malicious inputs to bypass access control?
- Contract Initialization:
- Is the contract properly initialized (e.g., setting the owner in the constructor)?
- Can an attacker call an initialization function multiple times to reset ownership or state?
- Are there unprotected functions that can be called before initialization?
- External Interactions:
- Do functions call external contracts or delegate control (e.g., via call or delegatecall)? If so, are these calls restricted to authorized users?
- Can external contracts manipulate the contract’s state due to missing access controls?
- Event Logging and Transparency:
- Are critical actions (e.g., ownership changes, fund withdrawals) logged with events?
- Does the absence of events for sensitive actions indicate a lack of proper access control or oversight?
- Standard Practices and Libraries:
- Does the contract use established access control libraries like OpenZeppelin’s Ownable or AccessControl?
- If custom access control logic is used, is it thoroughly tested and equivalent to standard implementations?
- Are there deviations from best practices that could introduce vulnerabilities?
- Edge Cases and Exploits:
- Can a non-owner call a sensitive function under specific conditions (e.g., during contract deployment or after a state change)?
- Are there functions that implicitly trust msg.sender without verifying its legitimacy?
- Could an attacker exploit a lack of access control to drain funds, change ownership, or lock the contract?
- Testing and Auditing:
- Have sensitive functions been tested with non-authorized accounts to ensure they revert as expected?
- Has the contract been audited or analyzed with tools like Slither, Mythril, or Oyente for access control issues?
- Do test cases cover scenarios where unauthorized users attempt to call restricted functions?
How to Use These Questions
- Code Review: Systematically go through the contract’s code, asking these questions for each function, especially those marked public or external.
- Tool Assistance: Use static analysis tools (e.g., Slither) to flag missing access controls, then verify findings with these questions.
- Manual Testing: Deploy the contract on a testnet and attempt to call sensitive functions from non-authorized accounts to see if they succeed.
- Compare with Standards: Check if the contract aligns with secure templates (e.g., OpenZeppelin’s Ownable) to ensure proper access control