Skip to main content

access-control

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

Aller à l'installation

Informations de source

Dépôt
BitterSecurity/Decepticon
Dernière activité de la source
2 juin 2026 à 17:25
Langue détectée de SKILL.md
anglais
Étoiles
5 565
Forks
1 053

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
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)
Voir sur GitHub