Skip to main content

access-control

Missing modifiers, wrong msg.sender checks, default-public functions, missing onlyOwner / onlyRole / onlyDAO authorization.

Ir a la instalación

Datos de origen

Repositorio
BitterSecurity/Decepticon
Última actividad en el origen
2 de junio de 2026 a las 17:25
Idioma detectado de SKILL.md
inglés
Estrellas
5565
Forks
1053

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
access-control
description
Missing modifiers, wrong msg.sender checks, default-public functions, missing onlyOwner / onlyRole / onlyDAO authorization.
metadata
{"subdomain":"smart-contracts","when_to_use":"smart contract access control onlyowner missing modifier privilege","mitre_attack":["T1190"]}
# Access Control Playbook Access control bugs are the most boring class — and the most common in production audits. They're cheap to find with grep + LSP. They drain millions when missed (LeetSwap, Audius, Saddle, Akropolis). ## Audit steps ### 1. Find every state-changing function ```bash # Functions that are NOT view/pure and NOT internal/private grep -rE 'function [a-zA-Z_]+\(.*\)(public|external)' src/ | grep -v 'view\|pure' # Or via slither slither . --print function-summary ``` ### 2. For each external/public state-changer, ask - Does it modify storage that affects user funds, ownership, or configuration? - Is there a modifier (`onlyOwner`, `onlyRole`, `onlyDAO`, custom auth)? - If yes, what's the modifier checking? - If no, should there be one? ### 3. Audit each modifier Common modifier patterns + bugs: | Pattern | Bug | |---|---| | `require(msg.sender == owner)` | `owner` settable by anyone? `setOwner` unprotected? | | `require(msg.sender == tx.origin)` | Wrong — tx.origin breaks meta-transactions, AND it's phishable | | `_msgSender()` in OZ ERC2771Context | Forwarder trusted but anyone can forward — does the contract validate the forwarder? | | `onlyRole(MINTER)` | Who can grant MINTER? Is the admin a multisig or single key? | | `require(initialized == false)` | Initializer can be called twice if `initialized` writeable elsewhere | | `require(block.timestamp > deployTime + 24 hours)` | Time-based is often a fake delay — check if `deployTime` is settable | | `require(approvedSigners[msg.sender])` | Approval list managed by single key? | ### 4. Specific anti-patterns to grep ```bash # Functions accidentally external (default in Solidity <0.5) grep -rn 'function [a-zA-Z_]*[^ ]* *{' src/ | grep -v 'internal\|private\|public\|external' # msg.sender == tx.origin (phishable) grep -rn 'tx.origin' src/ # Reentrancy in access-control checks grep -rn 'onlyOwner.*nonReentrant' src/ # both? often wrong order # `delegatecall` without auth gate grep -rn 'delegatecall' src/ # `selfdestruct` available grep -rn 'selfdestruct\|suicide(' src/ ``` ### 5. Initializer bugs ```solidity // VULNERABLE function initialize(address _owner) external { owner = _owner; // missing: require(!initialized); initialized = true; } // VULNERABLE (proxy implementation) contract Impl { constructor() { ... } // NEVER RUNS in proxy context // initialize() should set proxy state but doesn't gate } // SAFE function initialize(address _owner) external initializer { // OZ's initializer modifier enforces single-call __Ownable_init(_owner); } ``` For UUPS/Transparent proxies: check `_authorizeUpgrade` is overridden and gated. Default OZ override is empty (revert). ### 6. Function-selector collisions (proxies) Transparent proxy admin function selectors collide w/ impl function selectors → admin functions become uncallable, OR impl functions are shadowed by admin. Check w/ slither: ```bash slither-check-erc src/Impl.sol --erc ERC1967 ``` ### 7. Role grant/revoke For `AccessControl`: - `DEFAULT_ADMIN_ROLE` is the admin of all roles by default - Anyone with DEFAULT_ADMIN_ROLE can grant any role to anyone - If init code grants DEFAULT_ADMIN_ROLE to deployer w/ no transfer to multisig → key person risk Check role hierarchy: ```bash grep -rn '_setRoleAdmin\|_setupRole\|grantRole' src/ ``` ## PoC template (Foundry) ```solidity function test_unauth_call() public { address attacker = address(0xBEEF); vm.prank(attacker); // Call the function that should require auth target.dangerousFunction(arg1, arg2); // Assert state change happened assertEq(target.criticalParam(), expectedManipulatedValue); // No revert = vulnerable } function test_role_takeover() public { address attacker = address(0xBEEF); vm.startPrank(attacker); // If grantRole is callable by anyone target.grantRole(target.MINTER_ROLE(), attacker); assertTrue(target.hasRole(target.MINTER_ROLE(), attacker)); // Now exploit MINTER_ROLE target.mint(attacker, 1_000_000 ether); } function test_init_re_entry() public { // Call initialize twice target.initialize(address(this)); vm.expectRevert("Initializable: contract is already initialized"); target.initialize(attacker); } function test_upgrade_no_auth() public { address attacker = address(0xBEEF); address malicious = address(new MaliciousImpl()); vm.prank(attacker); target.upgradeTo(malicious); // Now any call goes to malicious — drain assertEq(target.totalSupply(), 0); } ``` ## Severity calibration | Bug | Severity | |---|---| | Unauth `withdraw` / `mint` / `transferOwnership` | Critical | | Unauth `upgradeTo` (proxy) | Critical (escalates to any) | | Unauth `setOracle` / `setFee` | High (DoS or value manipulation) | | `tx.origin` auth + phishable | High | | Initializer re-entry | High (proxy takeover) | | Role hierarchy lets non-admin grant admin | High | | Function defaults to external (Solidity <0.5 only) | High | | Selector collision in proxy | High (function unreachable) | ## CVSS - Unauth drain function: 9.8-10.0 - Unauth upgrade: 10.0 - Unauth admin / role-change: 9.0 - DoS via unauth pause: 7-8 ## Defender remediation ```solidity // Use OpenZeppelin patterns import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; contract MyContract is AccessControlUpgradeable, UUPSUpgradeable { bytes32 public constant MINTER = keccak256("MINTER"); function initialize(address admin) external initializer { __AccessControl_init(); _grantRole(DEFAULT_ADMIN_ROLE, admin); } function mint(address to, uint256 amt) external onlyRole(MINTER) { _mint(to, amt); } function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} } ``` ## Known exemplars - Audius (Jul 2022): $6M; unauth initialize on governance proxy - LeetSwap (Aug 2023): $625k; unauth swap fn enabled draining - Saddle (Apr 2022): $11M; rounding + access combo - Akropolis (Nov 2020): $2M; access + reentrancy combo - Pickle Finance (Nov 2020): $20M; missing access on the jar - Wormhole (Feb 2022): $325M; signature verification bug — adjacent (sig-replay/SKILL.md) - Nomad (Aug 2022): $190M; missing message-merkle-root validation - Multichain (Jul 2023): $126M; private-key compromise (not strictly access-control, but exploited admin)
Ver en GitHub