本文介绍CheckMarx安全研究小组通过扫描公开的以太坊智能合约所发现的Solidity智能合约开发中常见的十大安全问题,其中__未检查的外部调用__ 和 高成本循环 分列排行榜前两名。该安全问题排行榜于2020年5月发布。
在2018年,CheckMarx安全研究小组首次发布了Solidity智能合约开发中常见的十大安全问题排行榜,其中位居榜首的两个问题分别是__外部合约拒绝服务__ 和 重入 。2020年的情况已经有所变化,
下表比较了2018年和2020年Solidity十大常见安全问题之间的变化。这些问题按严重程度和流行程度排序:
用自己熟悉的语言学习 以太坊DApp开发 : Java | Php | Python | .Net / C# | Golang | Node.JS | Flutter / Dart
S1 – 未检查的外部调用
这是我们之前的Solidity十大安全问题榜单上第三常见的问题。由于现在解决了前两个问题,因此 未检查的外部调用 已升至2020年更新列表中最常见的问题。
Solidity低级调用方法,例如address.call()
,不会引发异常。相反,如果调用 遇到异常,则它们返回false
。另一方面,如果 doSomething()
抛出异常,则合约调用(例如 ExternalContract.doSomething()
)会自动传播异常。
使用addr.send()
发送以太币是一个很好的例子,应该通过检查返回值来显式处理不成功的发送,但这对于其他外部调用也有效:
S2 – 高成本循环
代价高昂的循环从Solidity安全榜单的第四名上升至第二名。尽管我们以前的清单中的前2个问题都已解决,但受该问题影响的智能合约数量却增长了近30%。
以太坊环境的算力是需要付费的(使用以太币)。因此,减少完成操作所需的计算步骤不仅是优化的问题,而且还涉及成本效率。
循环是昂贵操作的一个很好的例子:由于数组中包含许多元素,因此需要更多迭代才能完成循环。如你所料,无限循环会耗尽所有可用GAS。
如果攻击者能够影响元素数组的长度,则上述代码将导致拒绝服务,从而阻止执行跳出循环。尽管它与十大常见问题相去甚远,但在扫描的智能合约中发现有8%的合约存在数组长度操纵问题。
S3 – 权力过大的所有者
这是Soldiity十大安全问题榜单中的一个新条目,该问题影响了约16%的已扫描智能合约。
某些合约与其所有者(Owner)紧密相关,使得某些功能只能由所有者地址调用,如下例所示:
只有合约所有者能够调用doSomething()
和doSomethingElse()
函数:前者使用onlyOwner
修饰符,而后者则显式执行该修饰符。这带来了严重的风险:如果所有者的私钥遭到泄露,则攻击者可以控制该合约。
S4 – 算术精度问题
由于使用256位虚拟机(EVM),因此Solidity的数据类型有些复杂。该语言不提供浮点表示形式,并且少于32个字节的数据类型将被打包到同一个32字节的槽位中。考虑到这一点,你应该会想到可能存在的精度问题:
如上例所示,在乘法之前执行除法时,可以预料存在很大的舍入误差。
S5 – 过于依赖tx.origin
智能合约不应依赖于tx.origin
进行身份验证,因为恶意合约可能会进行中间人攻击,耗尽所有资金。
建议改用msg.sender
:
可以在Solidity的文档中查看Tx Origin攻击 的详细说明。简单的说,tx.origin
始终是合约调用链中的第一个帐户,而msg.sender
则表示直接调用者。如果链中的最后一个合约依赖于tx.origin
进行身份验证,那么调用链中间环节的合约将能够榨干被调用合约的资金,因为身份验证没有检查究竟是谁(msg.sender
)进行了调用。
S6 – 上溢出/下溢出
Solidity的256位虚拟机(EVM)存在上溢出和下溢出问题,具体演示可以查看这里。在for
循环条件中使用uint
数据类型时,开发人员要格外小心,因为它可能导致无限循环:
在上面的示例中,当i
的值为0
时,下一个值为2^256 -1
,这使条件始终为true
。开发人员应当尽量使用<
、>
、!=
和==
进行比较。
S7 –不安全的类型推断
该问题在Solidity十大安全问题排行榜中上升了两个位置,现在影响到的智能合约比之前多出17%以上。
Solidity支持类型推断,但有一些奇怪的表现。例如,字面量0
会被推断为byte
类型,而不是我们通常期望的整数类型。
在下面的示例中,i
的类型被推断为uint8
,因为这时能够存储i
的值的最小长度的整数类型。但如果elements
数组包含256个以上的元素,则下面的代码就会发生溢出:
因此,我们建议显式声明数据类型,以避免代码意外的行为或错误。
S8 – 不正确的转账
此问题在Solidity十大安全问题榜单中从第六位下降到第八位,目前影响不到1%的智能合约。
在合约之间进行以太币转账有多种方法。虽然官方推荐使用addr.transfer(x)
函数,但我们仍然找到了还在使用send()
函数的智能合约:
请注意,如果转账不成功,则addr.transfer(x)
会自动引发异常,这样可以减轻了先前讨论的未检查外部调用的问题S1。
S9 –循环内转帐
当在循环中进行以太币转账时,如果其中一个合约不能收到,那么整个交易将被回滚。
攻击者可能利用此行为来导致拒绝服务,从而阻止其他合约接收以太币。
S10 – 时间戳依赖性
在Soldity十大安全问题榜单中,该问题在2018年排名第五。
重要的是要记住,智能合约在不同时刻在多个节点上运行。以太坊虚拟机(EVM)不提供时钟时间,并且通常用于获取时间戳的now
变量实际上是矿工可以操纵的环境变量(block.timestamp
的别名)。
由于矿工当前可以操纵环境变量,因此只能在不等式>
、<
、>=
和<=
中使用其值。
如果你的应用需要随机性,可以参考RANDAO合约,该合约基于任何人都可以参与的去中心化自治组织(DAO),是所有参与者共同生成的随机数。
总结
在比较2018年和2020年的Solidity十大常见安全问题列表时,我们可以观察到有关开发最佳实践的一些进展,尤其是那些影响安全性的实践。看到2018年排名前2的问题,__外部合约拒绝服务__ 和 重入 离开前十名榜单是一个积极信号,但开发者仍然需要采取一些重要步骤来避免常见错误。