Skip to main content
Sigvex

Access Control

Detects missing or improperly implemented access controls on privileged functions, allowing unauthorized callers to execute restricted operations.

Access Control

Overview

Remediation Guide: How to Fix Access Control Vulnerabilities

The access control detector identifies functions that modify sensitive state — ownership, admin roles, funds, or protocol parameters — without verifying the caller’s authorization. Sigvex analyzes the data-flow graph to determine whether msg.sender or tx.origin is compared against a stored privileged address before any sensitive operation executes.

This detector flags common patterns including: unprotected selfdestruct, unguarded transferOwnership, missing onlyOwner modifiers on withdrawal functions, and privileged state setters without a caller check. According to on-chain incident data, missing access controls account for a significant share of smart contract exploits annually, with the Ronin bridge hack ($625M, March 2022) being a prominent example of insufficient multi-signature authorization.

Why This Is an Issue

Access control vulnerabilities are among the highest-impact vulnerability class in smart contracts. An attacker who can call any function on a contract — including drain-ether, upgrade-proxy, or set-admin functions — can take complete control of the protocol’s assets and governance. Unlike reentrancy, access control bugs often require only a single transaction and leave no artifacts for defenders.

The pattern frequently appears in: newly deployed contracts that forget to port modifiers from prior versions, contracts copied from templates where the ownership model changed, and upgradeable proxies where the _authorizeUpgrade function is left unprotected.

How to Resolve

// Before: Vulnerable — no caller check
function withdrawAll() external {
    payable(msg.sender).transfer(address(this).balance);
}

// After: Fixed — caller must be owner
address public owner;

modifier onlyOwner() {
    require(msg.sender == owner, "Not owner");
    _;
}

function withdrawAll() external onlyOwner {
    payable(owner).transfer(address(this).balance);
}

For role-based access control, use OpenZeppelin’s AccessControl:

import "@audited/access/AccessControl.sol";

contract ProtocolAdmin is AccessControl {
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

    constructor() {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(ADMIN_ROLE, msg.sender);
    }

    function emergencyPause() external onlyRole(ADMIN_ROLE) {
        // safe: only ADMIN_ROLE holders can call
    }
}

Examples

Vulnerable Code

contract VulnerableProxy {
    address public implementation;
    address public admin;

    // Missing access check — anyone can upgrade the implementation
    function upgradeImplementation(address newImpl) external {
        implementation = newImpl;
    }

    // Missing access check — anyone can drain ETH
    function emergencyWithdraw() external {
        payable(msg.sender).transfer(address(this).balance);
    }
}

Fixed Code

contract SecureProxy {
    address public implementation;
    address public admin;

    modifier onlyAdmin() {
        require(msg.sender == admin, "AccessControl: not admin");
        _;
    }

    function upgradeImplementation(address newImpl) external onlyAdmin {
        require(newImpl != address(0), "Invalid implementation");
        implementation = newImpl;
    }

    function emergencyWithdraw() external onlyAdmin {
        payable(admin).transfer(address(this).balance);
    }
}

Sample Sigvex Output

{
  "detector_id": "access-control",
  "severity": "high",
  "confidence": 0.88,
  "description": "Function upgradeImplementation() writes to storage slot 0x0 (implementation address) without verifying msg.sender against a privileged address.",
  "location": { "function": "upgradeImplementation(address)", "offset": 64 }
}

Detection Methodology

  1. Identify sensitive operations: The detector looks for writes to storage slots associated with ownership/admin variables, selfdestruct, ETH transfers, and proxy upgrade calls.
  2. Trace caller checks: Using data-flow analysis, it checks whether CALLER (opcode for msg.sender) or ORIGIN is compared (EQ, NEQ) against any storage value in the function’s execution path before the sensitive operation.
  3. Evaluate sufficiency: It distinguishes between hardcoded address checks (high confidence), dynamic owner variable checks (medium-high confidence), and no checks at all (highest confidence of a true positive).
  4. Apply context: Functions named initialize, constructor, or that are only reachable via delegatecall are given lower confidence scores to reduce false positives.

Limitations

False positives:

  • Functions protected by a custom modifier whose implementation is in a separate function call may not be fully resolved, resulting in false positives.
  • Upgradeable proxy contracts may implement access control in the proxy layer, invisible when analyzing the implementation contract alone.
  • Functions that use time-locks or multi-sig patterns rather than direct caller checks may be flagged.

False negatives:

  • Access controls implemented via assembly (sload followed by eq) may not be recognized as owner checks without pattern matching.
  • Cross-contract access delegation (e.g., “only this sister contract can call”) is not analyzed without cross-contract analysis.
  • Delegatecall — detects dangerous delegatecall from unprotected functions
  • Front-Running — detects ordering-sensitive patterns exploitable by unauthorized callers

References