掌握 Solidity 中的访问控制

本文深入探讨了Solidity智能合约中访问控制的重要性,以及如何通过适当的访问控制机制来防御潜在的安全漏洞。

🔐 精通 Solidity 中的访问控制:🚀 开发者和安全研究人员的终极指南 🛡️

1. 简介

在区块链的世界里,代码即法律 —— 一旦部署,智能合约通常是不可变的,这意味着没有“撤销”按钮。这使得访问控制成为 Solidity 安全性最关键的支柱之一。它定义了谁可以在你的合约中做什么,保护着高影响力的操作,如铸造代币、升级系统、提取资金或暂停整个协议。

不幸的是,历史表明,访问控制缺陷是智能合约中最常见和最具破坏性的漏洞之一。一个简单的遗漏 onlyOwner 检查、一个实施不完善的角色系统,甚至是一个被忽视的公共函数都可能成为攻击者敞开的大门。在某些情况下,这些疏忽导致了数百万美元的漏洞利用、完整的治理接管或整个协议的崩溃。

访问控制漏洞通常由于以下原因产生:

  • 缺少访问检查 —— 忘记限制敏感函数。
  • 不正确的访问逻辑 —— 有缺陷的角色分配或弱修饰符。
  • 过于宽泛的权限 —— 授予单个地址过多的权限。
  • 所有权管理不善 —— 忘记在部署后转移或保护所有权。

从本质上讲,Solidity 中的访问控制是为了确保只有授权的账户 —— 无论是外部拥有账户(EOA)、多重签名钱包还是治理合约 —— 才能执行敏感函数或更改关键状态变量。如果没有它,与区块链交互的任何人都可以直接调用这些函数,可能耗尽资金、改变系统逻辑或将合法用户拒之门外。

以太坊虚拟机(EVM) 不提供内置的权限层 —— 除非明确限制,否则每个函数都是公开的。这种“默认开放”的性质使得有意识的、精心设计的访问控制成为安全智能合约开发中不可协商的一部分

在本指南中,我们将:

  • 揭开 Solidity 中访问控制机制的神秘面纱。
  • 展示真实世界中存在漏洞的代码 以及攻击者如何利用它。
  • 提供使用 Remix 和 Foundry 的逐步 PoC(概念证明)
  • 与开发者们分享经过实战检验的最佳实践
  • 高亮显示安全研究人员在审计期间需要注意的危险信号

无论你是希望增强合约安全性的 Solidity 开发者,还是寻找漏洞的安全研究人员,本博客都将为你提供知识和实践技能,以便在攻击者找到钥匙之前锁定你的智能合约

2. Solidity 中的访问控制是什么?

从本质上讲,Solidity 中的访问控制是一种机制,它决定了谁被允许在智能合约中执行特定操作。这类似于传统系统中某些操作只允许管理员执行,而其他操作对所有用户开放。

然而,与传统后端系统中通常内置于框架中的访问控制不同,以太坊虚拟机(EVM) 没有内置的访问限制层。默认情况下,Solidity 合约中的每个函数都是公开的,除非另有明确标记 —— 这意味着任何人都可以通过交易直接调用它,即使没有前端界面。

当出现以下情况时,会发生访问控制漏洞

  • 应该被限制的函数或资源是公开可访问的
  • 该函数没有得到充分的保护(例如,使用有缺陷的逻辑或弱修饰符)。

当访问控制被破坏时,未经授权的用户可以充当特权账户,从而导致灾难性的结果。一些常见的攻击者能力包括:

铸造/销毁代币 —— 打印无限的代币或销毁现有的代币。

提取协议资金 —— 清空流动性池或国库储备。

更改合约所有权 —— 完全接管协议。

暂停/取消暂停系统 —— 冻结操作或启用恶意更新。

修改治理设置 —— 更改投票规则或法定人数阈值。

升级合约逻辑 —— 将恶意代码注入可升级合约。

示例场景

想象一下一个DeFi 借贷协议,它有一个函数:

开发人员计划 此函数仅由管理员 调用,因为利率直接控制协议的经济状况。

在你的收件箱中获取 vishhxyz 的故事

但由于它没有访问限制

  • 任何攻击者都可以调用 setInterestRate(0) 使贷款免费。
  • 或者调用 setInterestRate(99999) 使借款成为不可能。
  • 在这两种情况下,协议的财务模型都会立即崩溃。

主要收获

访问控制在 Solidity 中不是可选的 —— 它是第一道防线。一个简单的 onlyOwner 修饰符的遗漏可能就像把银行的金库门敞开一样危险。

3. 真实案例研究

1. Parity 多重签名钱包攻击 (2017)

2. Level Finance 漏洞利用 (2023)

3. OpenZeppelin ERC777 事件 (2019)(注意:虽然不是完整的漏洞利用,但它是不受保护的Hook导致意外行为的一个关键例子。)

这里有什么问题?

为了真正理解缺少或有缺陷的访问控制有多么危险,让我们通过一个最小的漏洞利用示例。在这种情况下,有漏洞的合约允许任何人调用一个 burn() 函数,而不验证谁被允许执行销毁操作。

5. 概念证明(PoC)攻击

此接口表示目标合约,该合约应该在允许从特定账户销毁代币之前需要权限检查。不幸的是,在我们的场景中,它没有 —— 没有 onlyOwner没有基于角色的访问控制,也没有发送者验证。

🚨 攻击如何运作

  • 侦察 —— 攻击者发现 burn() 函数没有访问控制检查
  • 设置 —— 他们部署恶意的 AccessControlAttacker 合约。
  • 执行 —— 他们使用以下参数调用 attack()
    • target → 有漏洞的合约的地址。
    • victim → 持有代币的合法用户的地址。
  • 造成的损害 —— 合约运行 burn(victim, 100),立即从受害者的余额中删除 100 个代币

💥 影响

  • 💸 直接损失 —— 受害者在未经同意的情况下损失代币。
  • 🌊 DeFi 中的连锁反应 —— 如果该代币被用作抵押品,这可能会导致清算、资不抵债或稳定币脱锚
  • 🗳 治理破坏 —— 在治理代币中,攻击者可以在关键投票前减少对手的投票权

💡 得到的教训

每个更改余额、所有权或关键参数的函数都必须具有严格的访问控制。

即使是“非金融”函数也可能成为攻击者手中的强大武器。

✅ 6. 安全实施(使用 OpenZeppelin)

为了防止未经授权的操作,我们可以利用 OpenZeppelinOwnable 合约,该合约提供了一个经过实战检验的所有权模型,带有 onlyOwner 修饰符。这确保了只有合约所有者才能执行关键函数,例如销毁代币。

🔒 发生了什么变化?

  • 访问限制 —— onlyOwner 修饰符阻止未经授权的用户调用 burn()
  • 明确的所有权模型 —— 所有权在部署时分配,并且可以安全地转移。
  • 最小的攻击面 —— 敏感逻辑被包装在内部函数( _burn)中,并在入口点受到保护。

💡 给开发者的专业提示

  • 如果需要多个角色(例如, AdminMinterPauser),请考虑使用 OpenZeppelin 中的 AccessControl,而不仅仅是 Ownable
  • 始终在部署脚本中验证所有权更改 —— 忘记将所有权转移到多重签名或 DAO 可能与没有访问控制一样危险。

🔑 7. Solidity 中的访问控制模式

在保护智能合约方面,没有一种尺寸适合所有情况。不同的协议需要不同的访问控制策略,具体取决于复杂性、风险级别和治理模型。以下是三种最常见的模式:

(a)Ownable —— 简单有效

  • 📝 描述:最简单的访问控制形式 —— 单个地址(所有者)可以执行受限函数。
  • 📦 实施:由 OpenZeppelinOwnable 合约提供。
  • 优点:易于实施,开销最小。
  • ⚠️ 缺点:单点故障 —— 如果所有者密钥泄露,整个合约都将面临风险。
  • 示例
function pause() public onlyOwner { ... }

(b)基于角色的访问控制(RBAC)—— 灵活且可扩展

  • 📝 描述:将不同的角色分配给不同的地址 —— 例如, ADMIN_ROLEMINTER_ROLEPAUSER_ROLE
  • 📦 实施:由 OpenZeppelinAccessControl 合约提供。
  • 优点:细粒度的权限控制,非常适合大型团队或 DAO。
  • ⚠️ 缺点:实施和管理稍微复杂一些。
  • 示例
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { ... }

(c)多重签名访问 —— 无单点故障

  • 📝 描述:关键函数需要多个地址批准才能执行。
  • 📦 实施:通常通过 Gnosis Safe 或自定义多重签名合约处理。
  • 优点:高度安全 —— 阻止单个泄露的密钥触发关键操作。
  • ⚠️ 缺点:由于需要多次批准,执行速度较慢。
  • 示例
    • 要升级合约,至少需要 5 个签名者中的 3 个 批准。

8. 测试访问控制

🐞 概念验证(PoC)—— 可升级合约中未初始化的访问控制

设置

我们有一个可升级的合约,它:

  • 使用 OpenZeppelinAccessControlUpgradeable 进行角色管理。
  • 旨在仅允许 KEEPER_ROLE 运行敏感函数。
  • 初始化器(而非构造函数)中分配管理员角色。

错误:

  • 该合约通过代理部署,但从未初始化
  • 或者,开发人员忘记在实现合约中调用 _disableInitializers()

看起来不错,对吧?

但是,如果部署后没有人调用 initialize(),则没有设置任何管理员 —— 这意味着任何人都可以先调用它并声明所有角色。

攻击

步骤 1 —— 攻击者注意到合约未初始化

hasRole(DEFAULT_ADMIN_ROLE, attackerAddress) // 返回 false

...并且看到没有人拥有该角色

步骤 2 —— 攻击者成为管理员

initialize(attackerAddress)

现在:

  • attackerAddress = DEFAULT_ADMIN_ROLE
  • attackerAddress = KEEPER_ROLE

步骤 3 —— 滥用特权

  • 调用 setSecret() 或任何其他 onlyRole(KEEPER_ROLE) 函数。
  • 授权升级并用恶意代码替换逻辑。

影响

  • 🚨 完全接管协议 —— 攻击者可以更改逻辑、耗尽资金或将用户拒之门外。
  • 💀 永久性损害 —— 可升级的合约允许攻击者插入的代码无限期地存在。

🛡 9. Solidity 中访问控制的最佳实践

使用经过验证的模式 —— 使用 OpenZeppelin 中的 OwnableAccessControl 实施访问控制。

限制敏感函数 —— 使用 onlyOwner、角色检查或自定义修饰符来保护高影响力函数。

安全初始化 —— 对于可升级的合约,请确保初始化器是一次性的受到适当限制

定期审计 —— 在每次重大代码更改后审查和测试访问控制逻辑。

最小化公共攻击面 —— 避免公开可能影响协议安全性的公共函数。

使用多重签名进行治理 —— 将 admin/升级权限分配给生产环境中的多重签名钱包

> 案例研究驱动

“智能合约中的访问控制:模式、陷阱和真实漏洞”

智能合约漏洞数据集 - Cyfrin Solodit

智能合约漏洞数据集 - Cyfrin Solodit

智能合约漏洞数据集 - Cyfrin Solodit

智能合约漏洞数据集 - Cyfrin Solodit

10. 结论

访问控制漏洞易于预防,但被忽视时会造成毁灭性打击。在许多历史漏洞利用中,根本原因不是复杂的加密缺陷 —— 而是缺少角色检查、忘记了 onlyOwner 或未初始化的合约。

通过遵循最小权限原则,将敏感操作限制在最少的必要账户,并使用经过实战检验的库(如 OpenZeppelin),你可以大大减少攻击面。

安全性不会在部署时结束 —— 持续的审计、测试和操作规范 至关重要。从其他协议花费数百万美元的错误中吸取教训,并构建你的智能合约,以便攻击者无从下手。

在 Solidity 中,一切都是公开的,除非你将其设为私有。

保护你的函数,就像它们持有你的协议的钥匙一样 —— 因为它们确实如此。

  • 原文链接: blog.blockmagnates.com/m...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
blockmagnates
blockmagnates
The New Crypto Publication on The Block