为什么你的代码需要是自包含的

本文探讨了在智能合约开发中应用单一责任原则(SRP)的重要性,通过两个实际案例,展示了函数承担多个职责可能导致的安全漏洞,包括通过抢跑攻击植入后门和通过销毁合约拒绝核心功能。文章强调了通过强制用户显式注册命名空间来简化函数,提高代码的可维护性和可预测性,从而减轻这些风险。

简介

安全应用程序的主要支柱之一是编写简洁的代码。这通过许多原则来实践,忽视这些原则通常会导致不必要的攻击媒介,从而导致安全问题。Robert C. Martin 在 SOLID 原则中制定了简洁代码的基线之一。第一个原则是单一职责原则 (SRP),或者用 “Uncle Bob” 的话说:

一个类应该只有一个改变的原因。

换句话说,一个类应该封装一个单一的、定义明确的关注点或功能,从而促进模块化和可维护的代码。一个类是 自包含的

这个原则以 1 对 1 的方式Object-oriented programming转换为智能合约开发。例如,作为 OpenZeppelin Library 5.0 版本的一部分发布的 AccessManager 合约就是一个很好的自包含合约示例,它能够一次性授权多个合约。相反,AccessControl 合约由相应的合约直接继承,因此对授权的更改将需要,例如,token 合约进行更改以添加新的 mint 角色。这个概念在函数级别同样适用:一个函数应该只负责一个动作。总的来说,SRP 具有以下优点:

  1. 简洁性: 通过编写简洁的代码,避免了不必要的复杂性,从而减少了攻击面。这使得更容易理解代码的目的和功能。
  2. 可预测性: 由于每个合约或函数只有一项任务要处理,因此它们的使用将是可预测的。这使得评估调用链的可能结果变得更容易。
  3. 可维护性: 代码更容易修改、更新和扩展,同时减少对智能合约其他部分的影响。这也使得故障排除和测试更容易。

案例研究

在下文中,我们将通过两个关键审计发现的例子来介绍函数自包含代码的重要性。如上所述,这两个问题都源于一次处理两个任务的函数。这种攻击媒介可以通过两种方式利用:首先,进行抢跑攻击以安装后门,其次,销毁合约以拒绝核心功能。

被审计的项目 是 MUD 框架,它实现了 StoreWorld 模块。World 类似于 Diamond Proxy 的设计,它充当系统(即 facets)的入口点。这些系统可以实现任意逻辑来与 World 的存储交互,而 Store 模块使这种存储能够以表格格式处理。因此,World 作为 Store 之上的抽象层。用户可以以无需许可的方式在此 World 中注册命名空间。此命名空间所有权允许创建者添加和操作表,添加系统,转移余额以及管理对此命名空间或其资源的访问控制。

问题 1:抢跑后门

对于第一个问题,我们想看看 registerSystem 函数。这段代码负责注册系统,以便可以通过 World 寻址,并有权访问命名空间的表和系统。但是,registerSystem 函数并非完全自包含。在 if-else 条件中存在一个检查,其中将自动注册一个不存在的命名空间,同时对现有命名空间执行访问控制检查。虽然这是一种更好的用户体验,但也允许发生以下攻击。

由于项目的无需许可性质,第一个分支使攻击者能够抢在受害者的交易之前,使用受害者的命名空间调用 registerNamespace。这将使攻击者成为命名空间所有者。在同一个攻击者交易中,对更多攻击者帐户授予对命名空间的进一步访问权限。之后,命名空间的所有权被转移给受害者。然后,受害者的交易执行 else 分支,同时也通过了访问控制检查。受害者可能不知道的是,仍然有一个攻击者可以控制其命名空间的表和余额。

抽象的registerSystemregisterNamespace函数的WorldRegistrationSystem

问题 2:销毁的合约

对于第二个问题,我们将研究 registerTable 函数。在我们深入研究此问题的详细信息之前,值得注意的是,此函数同样受到第一个问题的影响。再次,通过 if 条件对命名空间的存在执行检查。如果注册表的命名空间尚不存在,则攻击者能够通过与上述相同的攻击来获得访问权限。

要更好地理解此问题,我们需要更深入地研究 World 的设计。鉴于 World 通过命名空间和系统运行,因此该原则也影响核心功能,例如所讨论的注册,余额转移,访问管理等。因此,核心功能是通过在根命名空间下注册的核心系统实现的。由于核心功能需要在 World 的存储上运行,因此需要从 Worlddelegatecall 核心系统。然后,由于 WorldRegistrationSystemStoreRegistrationSystem 是核心系统父合约中的不同子合约,因此执行第二次 delegatecall 以从 registerTable 函数注册命名空间。

但是,由于核心系统是一个独立的合约,因此它具有自己的存储并且可以直接调用。这允许攻击者将任意合约注册为 CORE_SYSTEM_ID 系统。下一步,攻击者可以直接从核心系统调用 registerTable 函数,这将有效地 delegatecall 到攻击者控制的合约中,例如,执行 selfdestruct。这消除了 World 的所有核心功能。

抽象的registerTable函数的StoreRegistrationSystem

缓解措施

通过应用 SRP 可以缓解这两种攻击。这就像不从系统和表注册中调用命名空间注册一样简单,从而迫使用户显式地执行此操作。结果,这些函数将变得更加简洁,可维护和可预测。

在整个审计过程中,Lattice 团队的响应能力和解释的清晰度值得称赞。代码的组织和文档编制也值得注意。尽管发现了问题,但对系统的总体设计和实施进行了积极评估。

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

0 条评论

请先 登录 后评论
OpenZeppelin
OpenZeppelin
江湖只有他的大名,没有他的介绍。