Security·7 min का पठन·Solingo द्वारा

Access Control Vulnerabilities — Protect Your Admin Functions

अनुचित access control से millions drain हो सकते हैं। जानें कैसे admin functions को properly secure करें और common authorization mistakes से बचें।

# Access Control Vulnerabilities — Protect Your Admin Functions

Smart contracts में access control सबसे fundamental security mechanisms में से एक है। Improper access control से unauthorized users critical functions को execute कर सकते हैं — tokens mint कर सकते हैं, funds withdraw कर सकते हैं, या contracts को pause कर सकते हैं।

DeFi में कई major hacks खराब access control की वजह से हुए हैं। इस guide में, हम common vulnerabilities, best practices, और OpenZeppelin के battle-tested patterns सीखेंगे।

Access Control क्या है?

Access control यह determine करता है कि कौन कौन से functions को call कर सकता है और कब। यह typically involve करता है:

  • Authentication — caller की identity verify करना
  • Authorization — यह check करना कि caller को action perform करने की permission है या नहीं
  • Role management — different levels of access define करना

Common Vulnerabilities

1. Missing Access Control

सबसे basic mistake — critical functions पर कोई protection नहीं।

// VULNERABLE: Anyone can mint tokens!

contract VulnerableToken {

mapping(address => uint256) public balances;

uint256 public totalSupply;

function mint(address to, uint256 amount) external {

balances[to] += amount;

totalSupply += amount;

}

}

Attack: कोई भी unlimited tokens mint कर सकता है।

2. Weak tx.origin Check

tx.origin को authorization के लिए use करना dangerous है।

// VULNERABLE: tx.origin is not reliable

contract WeakAuth {

address public owner;

constructor() {

owner = msg.sender;

}

function withdraw() external {

require(tx.origin == owner, "Not owner");

payable(owner).transfer(address(this).balance);

}

}

Attack: Malicious contract owner को trick कर सकता है कि वह उसके through withdraw() call करे — tx.origin अभी भी owner होगा, लेकिन attacker funds receive करेगा।

Fix: tx.origin के बजाय msg.sender use करें।

3. Uninitialized Owner

Constructor में owner को set करना भूल जाना।

// VULNERABLE: Owner never set

contract UninitializedOwner {

address public owner;

function setOwner(address newOwner) external {

require(msg.sender == owner, "Not owner");

owner = newOwner;

}

}

owner address(0) है, तो कोई भी पहली बार setOwner() call कर सकता है और control ले सकता है।

4. Front-Running Owner Changes

Owner changes predictable हैं और front-run किए जा सकते हैं।

// VULNERABLE: Single-step ownership transfer

function transferOwnership(address newOwner) external {

require(msg.sender == owner);

owner = newOwner;

}

अगर owner address गलत है, तो contract permanently locked हो सकता है।

5. Incorrect Modifier Logic

Custom modifiers में logic errors।

// VULNERABLE: Logic error in modifier

modifier onlyOwner() {

require(msg.sender != owner, "Not owner"); // Wrong operator!

_;

}

!= को == होना चाहिए। यह modifier किसी को भी allow करता है owner को छोड़कर!

OpenZeppelin का Ownable Pattern

सबसे simple और widely-used access control pattern।

import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is Ownable {

mapping(address => uint256) public balances;

// Only owner can mint

function mint(address to, uint256 amount) external onlyOwner {

balances[to] += amount;

}

// Ownership transfer is two-step

// 1. Current owner calls transferOwnership(newOwner)

// 2. New owner calls acceptOwnership()

}

Ownable Features

  • ✅ Automatically sets deployer as owner
  • onlyOwner modifier for protected functions
  • ✅ Two-step ownership transfer (transferOwnership + acceptOwnership)
  • renounceOwnership for removing owner entirely

Ownable Implementation (Simplified)

abstract contract Ownable {

address private _owner;

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

constructor() {

_transferOwnership(msg.sender);

}

function owner() public view returns (address) {

return _owner;

}

modifier onlyOwner() {

require(owner() == msg.sender, "Ownable: caller is not the owner");

_;

}

function renounceOwnership() public onlyOwner {

_transferOwnership(address(0));

}

function transferOwnership(address newOwner) public onlyOwner {

require(newOwner != address(0), "Ownable: new owner is the zero address");

_transferOwnership(newOwner);

}

function _transferOwnership(address newOwner) internal {

address oldOwner = _owner;

_owner = newOwner;

emit OwnershipTransferred(oldOwner, newOwner);

}

}

Role-Based Access Control (RBAC)

Multiple roles के साथ complex permissions के लिए, OpenZeppelin का AccessControl use करें।

import "@openzeppelin/contracts/access/AccessControl.sol";

contract MultiRoleToken is AccessControl {

bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

mapping(address => uint256) public balances;

bool public paused;

constructor() {

// Grant deployer the default admin role

_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);

}

function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {

require(!paused, "Contract paused");

balances[to] += amount;

}

function burn(address from, uint256 amount) external onlyRole(BURNER_ROLE) {

require(balances[from] >= amount, "Insufficient balance");

balances[from] -= amount;

}

function pause() external onlyRole(PAUSER_ROLE) {

paused = true;

}

function unpause() external onlyRole(PAUSER_ROLE) {

paused = false;

}

// Admin can grant/revoke roles

function grantMinter(address account) external onlyRole(DEFAULT_ADMIN_ROLE) {

grantRole(MINTER_ROLE, account);

}

}

RBAC Benefits

  • ✅ Multiple independent roles
  • ✅ Role hierarchy (admin roles can manage other roles)
  • ✅ Fine-grained permissions
  • ✅ Easy to audit और manage
  • ✅ Separation of concerns

AccessControl Implementation (Simplified)

abstract contract AccessControl {

struct RoleData {

mapping(address => bool) members;

bytes32 adminRole;

}

mapping(bytes32 => RoleData) private _roles;

bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);

event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);

modifier onlyRole(bytes32 role) {

require(hasRole(role, msg.sender), "AccessControl: account missing role");

_;

}

function hasRole(bytes32 role, address account) public view returns (bool) {

return _roles[role].members[account];

}

function grantRole(bytes32 role, address account) public onlyRole(getRoleAdmin(role)) {

_grantRole(role, account);

}

function revokeRole(bytes32 role, address account) public onlyRole(getRoleAdmin(role)) {

_revokeRole(role, account);

}

function getRoleAdmin(bytes32 role) public view returns (bytes32) {

return _roles[role].adminRole;

}

function _grantRole(bytes32 role, address account) internal {

if (!hasRole(role, account)) {

_roles[role].members[account] = true;

emit RoleGranted(role, account, msg.sender);

}

}

function _revokeRole(bytes32 role, address account) internal {

if (hasRole(role, account)) {

_roles[role].members[account] = false;

emit RoleRevoked(role, account, msg.sender);

}

}

}

Time-Locked Admin Functions

Critical changes के लिए, time delay add करें ताकि users को react करने का time मिले।

import "@openzeppelin/contracts/governance/TimelockController.sol";

contract TimeLocked is Ownable {

TimelockController public timelock;

constructor(uint256 minDelay, address[] memory proposers, address[] memory executors) {

timelock = new TimelockController(minDelay, proposers, executors, address(0));

}

function criticalFunction() external {

require(msg.sender == address(timelock), "Must go through timelock");

// ... critical logic

}

}

Workflow:

  • Admin timelock में proposal submit करता है
  • minDelay (e.g., 2 days) wait करना होता है
  • उसके बाद ही proposal execute हो सकती है
  • Users को react करने का समय मिलता है (e.g., exit से पहले)
  • Multi-Signature Wallets

    Single owner से बचने के लिए, multi-sig wallets use करें।

    contract SimpleMultiSig {
    

    address[] public owners;

    uint256 public required;

    mapping(uint256 => mapping(address => bool)) public confirmations;

    struct Transaction {

    address to;

    uint256 value;

    bytes data;

    bool executed;

    }

    Transaction[] public transactions;

    modifier onlyOwner() {

    bool isOwner = false;

    for (uint i = 0; i < owners.length; i++) {

    if (owners[i] == msg.sender) {

    isOwner = true;

    break;

    }

    }

    require(isOwner, "Not owner");

    _;

    }

    constructor(address[] memory _owners, uint256 _required) {

    require(_owners.length >= _required && _required > 0, "Invalid requirements");

    owners = _owners;

    required = _required;

    }

    function submitTransaction(address to, uint256 value, bytes memory data)

    external

    onlyOwner

    returns (uint256)

    {

    uint256 txId = transactions.length;

    transactions.push(Transaction({

    to: to,

    value: value,

    data: data,

    executed: false

    }));

    return txId;

    }

    function confirmTransaction(uint256 txId) external onlyOwner {

    require(txId < transactions.length, "Transaction does not exist");

    require(!confirmations[txId][msg.sender], "Already confirmed");

    confirmations[txId][msg.sender] = true;

    }

    function executeTransaction(uint256 txId) external onlyOwner {

    require(txId < transactions.length, "Transaction does not exist");

    require(!transactions[txId].executed, "Already executed");

    uint256 count = 0;

    for (uint i = 0; i < owners.length; i++) {

    if (confirmations[txId][owners[i]]) {

    count++;

    }

    }

    require(count >= required, "Not enough confirmations");

    Transaction storage txn = transactions[txId];

    txn.executed = true;

    (bool success, ) = txn.to.call{value: txn.value}(txn.data);

    require(success, "Transaction failed");

    }

    }

    Production-ready multi-sigs: Gnosis Safe use करें।

    Testing Access Control

    Foundry Test Example

    contract AccessControlTest is Test {
    

    MyToken token;

    address owner = address(1);

    address alice = address(2);

    address bob = address(3);

    function setUp() public {

    vm.prank(owner);

    token = new MyToken();

    }

    function testOnlyOwnerCanMint() public {

    // Owner can mint

    vm.prank(owner);

    token.mint(alice, 100);

    assertEq(token.balances(alice), 100);

    // Non-owner cannot mint

    vm.prank(bob);

    vm.expectRevert("Ownable: caller is not the owner");

    token.mint(alice, 100);

    }

    function testOwnershipTransfer() public {

    vm.prank(owner);

    token.transferOwnership(alice);

    // New owner can now mint

    vm.prank(alice);

    token.mint(bob, 50);

    assertEq(token.balances(bob), 50);

    // Old owner cannot mint

    vm.prank(owner);

    vm.expectRevert("Ownable: caller is not the owner");

    token.mint(bob, 50);

    }

    }

    Best Practices Checklist

    • ✅ OpenZeppelin के battle-tested contracts use करें (Ownable, AccessControl)
    • msg.sender use करें, tx.origin never
    • ✅ Critical functions के लिए two-step ownership transfers implement करें
    • ✅ Complex permissions के लिए role-based access control use करें
    • ✅ Time locks add करें critical admin functions में
    • ✅ Production में multi-sig wallets prefer करें
    • ✅ हर modifier और access check को thoroughly test करें
    • ✅ Events emit करें हर permission change पर (auditability के लिए)
    • ✅ Zero address checks add करें ownership/role assignments में
    • ✅ Documentation में clearly define करें कि किसे क्या permissions हैं

    Real-World Example: Poly Network Hack (2021)

    August 2021 में, Poly Network से $600+ million drain हुए थे, जो तब तक का सबसे बड़ा DeFi hack था। Root cause एक access control vulnerability थी:

    • Contract ने _executeCrossChainTx() function को expose किया था
    • यह function किसी को भी arbitrary data के साथ call करने की permission देता था
    • Attacker ने इसे exploit करके खुद को keeper role assign कर लिया
    • Keeper permissions के साथ, attacker ने सारे funds drain कर दिए

    Lesson: सभी privileged functions को properly protect करें और extensive testing करें।

    Conclusion

    Access control smart contract security का backbone है। Simple ownership mistakes से लेकर complex role hierarchies तक, हर vulnerability devastating consequences ले सकता है।

    Key takeaways:

    • OpenZeppelin के proven patterns use करें
    • msg.sender use करें, tx.origin नहीं
    • Complex systems के लिए role-based access control implement करें
    • Critical changes के लिए time locks add करें
    • Multi-sig wallets से single points of failure eliminate करें
    • Extensively test करें सभी permission logic को

    Solingo पर, आप real-world scenarios के साथ access control vulnerabilities को identify और fix करना सीखेंगे। Production-grade authorization patterns master करें interactive challenges के साथ।

    अभी शुरू करें: solingo.io/security/access-control

    Practice में लगाने के लिए तैयार हैं?

    Solingo पर interactive exercises के साथ इन concepts को apply करें।

    मुफ्त में शुरू करें