Skip to main content

access-control

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

설치로 이동

소스 정보

저장소
BitterSecurity/Decepticon
최근 소스 활동
2026년 6월 2일 17:25
감지된 SKILL.md 언어
영어
스타
5,565
포크
1,053

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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)
GitHub에서 보기