被破坏的访问控制攻击发生在Solidity合约允许不受信任的调用者达到本应受到限制的状态更改能力时,例如转移资产、变更实现、赋予角色或配置受信任的地址。攻击者无需破解密码学:他们通过利用缺失、错误或过于宽泛的授权检查,从而实现有效的权限提升。
这种攻击可能影响整个协议,因为受权限控制的函数常常管理资金池或关乎治理的关键配置。Poly Network在2021年8月10日发生的一次事件中,跨越Ethereum、BSC和Polygon共失去6.11亿美元,被DefiLlama认定为“不当访问控制”。因此,授权不仅是UI界面或行政管理的细节,更是合约核心安全边界的重要组成部分。
什么是Solidity中的破坏性访问控制漏洞?
当合约未能可靠确认msg.sender是否获得了执行特权操作的权限,或者其授权模型赋予的权限超过了操作所需,就存在破坏性访问控制漏洞。在Solidity中,这类问题通常表现为外部可调用的函数,修改敏感存储、转移资产、升级代码或变更受信任方。
即使合约中定义了owner变量,也可能存在漏洞。关键在于每一条受权限保护的路径是否都正确执行了权限检查,包括辅助函数、初始化器、角色管理函数和升级钩子。
典型的破坏性访问控制攻击流程如下:
- 攻击者找到一个外部可调用且具有特权后果的函数。
- 该函数没有调用者检查、检查了错误的地址或依赖攻击者控制的状态。
- 攻击者直接调用该函数,绕过任何前端限制。
- 调用赋予某个角色、变更受信地址或触发资产转移操作。
- 攻击者利用新获得的能力提取资金或影响协议行为。
- 由于EVM只执行链上检验,合约会接受该交易。
前端按钮、文档或预设操作流程不能阻止直接调用合约。任何账户都可以向公共或外部函数提交call数据。因此,Solidity代码必须在每次关键状态变更时,强制执行政策。
哪些Solidity模式会导致访问控制攻击和权限提升?
如果开发者在暴露配置设置器、管理助手或初始化路径时没有充分考虑到每个公共函数都是潜在攻击的入口点,就容易出现不当访问控制漏洞。危险的模式不仅仅是完全缺失onlyOwner修饰符:还包括权限可被任意调用者修改、继承错误、应用不一致或操作权限过宽等情况。
常见的失误模式包括:
- 公开的设置器允许任何人更换金库、预言机、实现、操作员或受益人。
- 角色管理函数缺少管理员权限检查。
- 初始化器可以在部署后被调用,或由非预期账户调用。
- 使用
tx.origin而非msg.sender进行权限检查。 - 通过未保护的外部包装器调用内部受权限控制的函数。
- 单一管理员角色能执行所有操作,风险成倍增加。
- 权限基于用户提供的地址,而非不可变或受控的角色映射。
- 升级函数验证了调用者,但未验证新实现或升级路径的安全性。
这种潜藏的模式尤其隐晦,例如:
function setOperator(address newOperator) external {
operator = newOperator;
}
支付相关功能可能正确要求msg.sender == operator,但其权限边界已被突破,因为任何人都能自行设为操作员。审查者应从每个敏感函数出发,追溯可能获得其角色的路径。
Soken方法论将角色分配和受信地址配置视为本身具有权限的操作。安全的访问控制设计应保护最终资产转移的同时,也应严格限制任何可能解锁资产的路径。
破坏性访问控制在Solidity代码中的表现形式?
以下合约示例说明了典型的漏洞:它在支付前验证调用者,却使setPayoutOperator函数成为公共,攻击者可以自己设为操作员,从而调用pay实现提取资金的目的。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VulnerablePayoutVault {
address public payoutOperator;
constructor(address initialOperator) {
payoutOperator = initialOperator;
}
receive() external payable {}
function setPayoutOperator(address newOperator) external {
// 易受攻击:任何调用者都可以设为支付操作员
payoutOperator = newOperator;
}
function pay(address payable recipient, uint256 amount) external {
require(msg.sender == payoutOperator, "not operator");
require(address(this).balance >= amount, "insufficient balance");
(bool sent, ) = recipient.call{value: amount}("");
require(sent, "payment failed");
}
}
修正方案是将管理权限与操作隔离。只有合约拥有者才能赋予或收回“支付操作员”角色,而操作员不能管理角色或变更所有权。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract FixedPayoutVault {
address public owner;
mapping(address => bool) public payoutOperators;
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
modifier onlyPayoutOperator() {
require(payoutOperators[msg.sender], "not payout operator");
_;
}
constructor(address initialOwner, address initialOperator) {
require(initialOwner != address(0), "zero owner");
require(initialOperator != address(0), "zero operator");
owner = initialOwner;
payoutOperators[initialOperator] = true;
}
receive() external payable {}
function setPayoutOperator(address operator, bool allowed) external onlyOwner {
require(operator != address(0), "zero operator");
// 修正:只有所有者可以变更受信任角色
payoutOperators[operator] = allowed;
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "zero owner");
owner = newOwner;
}
function pay(address payable recipient, uint256 amount)
external
onlyPayoutOperator
{
require(address(this).balance >= amount, "insufficient balance");
(bool sent, ) = recipient.call{value: amount}("");
require(sent, "payment failed");
}
}
如果系统中存在角色变更、升级权限或金库提款的操作,建议进行访问控制重点的智能合约审计和安全评估,以追溯每条通路是否都受到合理限制。
Solidity开发者如何防范不当访问控制漏洞?
预防破坏性访问控制的关键在于明确权限模型,而非在功能实现后临时加入require语句。应定义每个敏感操作由哪个角色执行、授权原因及哪些子角色可以安全地完成日常任务。
| 技巧 | 能防止什么 | 额外的Gas或复杂度成本 | 局限性 |
|---|---|---|---|
onlyOwner修饰符 |
非授权的所有者专属操作 | 每次保护调用一次存储读取 | 被攻破的所有者仍拥有全部权限 |
角色映射或OpenZeppelin AccessControl |
非授权使用分离的操作角色 | 存储读取与角色管理逻辑 | 不良设计仍可能导致过度权限 |
| 最小权限原则 | 保持操作人或操作者仅拥有相关权限 | 需要定义多个角色 | 需严格界定操作边界 |
| 关键变更引入延时锁 | 避免立即恶意升级或参数更改 | 延迟执行与引入额外合约 | 若用户能应对延迟,仍可能遭受攻击 |
| 多签管理 | 单一密钥主动变更敏感状态 | 多签协调与操作额外成本 | 多签协调难度增加或策略弱化仍存风险 |
| 一次性初始化保护 | 防止重复或未授权初始化 | 存储标志与初始化逻辑 | 部署过程中权限设置仍需谨慎 |
建议使用经过审查的所有权和角色管理原语,但避免将库视作完整安全模型。例如,OpenZeppelin的Ownable提供所有权机制,具体是否赋予所有权以控制暂停、升级、费用收取、预言机更换或紧急措施,还需由协议团队决定。
避免使用tx.origin进行权限判断。因为它可能受到中介合约影响,不能直接识别调用者。应使用msg.sender,并结合预定义的所有者、角色、多签或合约特定授权规则进行验证。
哪些真实事件展示了访问控制攻击的影响?
根据DefiLlama统计,从2020年至2026年期间,涉及智能合约被归类为“访问控制不当”的事件多达130起,除交易所和钱包漏洞外,累计损失达9.097亿美元。这些已核实的事件表明,访问控制失败影响跨链桥、DeFi协议、机器人以及各类应用。
| 协议 | 日期 | 金额 | 链 |
|---|---|---|---|
| Poly Network | 2021-08-10 | $611.0M | Ethereum, BSC, Polygon |
| Mirror | 2021-10-08 | $90.0M | Terra |
| Wintermute | 2022-06-05 | $17.6M | Optimism |
| ValueDefi | 2021-05-05 | $10.0M | BSC |
| Cork V1 | 2025-05-28 | $12.0M | Ethereum |
| SUPERFORTUNE AI | 2026-05-27 | $15.2M | Ethereum |
| The Sandbox | 2026-08-22 | $675K | Base、BSC、Ethereum |
| Radix | 2026-08-31 | $1.2M | Radix |
这些事件有一个共同的教训:当合约的权限边界与其预期的信任模型不符时,协议的经济价值就会受到威胁。具体受影响的函数可能不同,但失败的类别相同:攻击者获得或行使了设计时本不应授予的权限。
如何在部署前检测破坏性访问控制漏洞?
检测这些漏洞需要映射敏感状态变更与对应的授权条件,然后验证不存在绕过条件的调用路径。自动化工具可以帮助识别可疑模式,但人工审查仍是关键,以理解业务授权、信任假设和权限提升的链条。
可使用以下检查清单:
- 列出所有涉及资产转移、角色变更、升级代码、暂停操作或配置受信地址的函数。
- 明确每个函数的预期调用方和授权规则。
- 跟踪每个可能授予、吊销、转移或初始化授权的函数。
- 检查所有能调用到受权限保护内部函数的外部包装器。
- 确认初始化只能执行一次,且按照预期的部署流程进行。
- 验证应急角色不能执行无关日常或治理行为。
- 测试角色吊销、所有权转移和零地址等异常场景。
- 将代理合约、继承合约及delegatecall路径纳入同一权限边界进行审查。
Slither能检测到缺失权限验证、危险的tx.origin用法及修饰符不一致等潜在问题,但发现结果需要人工验证。Foundry的模糊测试适合生成任意地址发起的调用,确保未授权调用者不能更改敏感状态。
Invariant(不变性)测试尤为有效。例如,可以定义存款保险不变性:没有支付角色的地址不得通过pay导致合约余额减少;或者只有所有者能修改角色成员资格。这类属性直接检验安全规则,而非仅测试典型用户流程。
安全洞察,Soken方法论:应将授权视为能力图谱,而非修饰符列表。每一个受权限保护的操作都应具有明确且可审计的路径,到达权限的每个路径都必须经过刻意设计,避免无意的权限提升路径。
通过明确权限,将权限最小化,严格保护赋予权限的函数,可以有效防止访问控制攻击。一个正确限制的提款函数如果让任何人都能任命提款者,也无法算安全。
首先构建你的合约(包括代理和操作账户)的权限图谱,然后将每条规则转化为修饰符和不变性测试。对于更深入的Solidity安全研究和其他相关漏洞类别,可以探索 Soken Hub 。