Skip to main content
 主页 > 区块链 >

智能合约开发必读:这 10 个 Solidity 安全问题不容

2020-12-09 18:01 浏览:

智能合约安全堪忧,了解 2020 年 Solidity 常见的 10 个安全问题。

原文标题:《Solidity 十大常见安全问题》
撰文:Erez Yalon
翻译:登链社区

在 2018 年,我们(CheckMarx)曾对智能合约安全状况进行过初步研究,重点是 Solidity[1] 编写的智能合约。当时,我们根据公开的合约源代码(译者注:本文称之为已扫描合约,本文出现的 x% 是以此为基数)编写了最常见的 10 个智能合约安全问题。两年过去了该更新研究并评估智能合约安全性发展的如何了。

值得关注的其他问题

尽管有一个安全问题排名很不错,但它往往一些有趣的细节,因为某些细节与排名列表并不完全一致。在深入挖掘 10 大问题之前,必要阐述一下原始研究中一些值得关注的亮点问题:

在 2018 年,最主要的两个问题是外部合约拒绝服务重入。但是现在这些问题有所缓解(不过依旧值得关注)。可以从我们的研究博客中了解更多有关 Reentrancy 的信息:从安全角度出发审视智能合约 [2]。

译者注:实际上由于 DeFi 应用之间的组合应用(例如闪电贷),又导致了多起严重的重入攻击事件。

现在 Solidity v0.6.x 发布 [3] 了,它带来了许多重大变化 [4],然而扫描的智能合约中有 50%甚至还没有准备好使用 Solidity v0.5.0 编译器。另外 30% 智能合约使用了过时的语法(例如:使用 sha3、throw 、constant 等),并且 83%的合约在指定编译器版本存在规范问题(pragma)。

译者注:Solidity 0.6 在语义上更明确了(例如 0.6 版本在继承方面的升级 [5]),有助于编译器及时发现问题,让代码更安全,

尽管可见性问题 [6] 没有出现在 2018 年的前 10 位,也没有出现今年的前 10,但可见性问题增加了 48%,值得关注。

下表比较了 2018 年和 2020 年十大常见问题列表之间的变化。这些问题按严重程度和流行程度排序:

智能合约开发必读:这 10 个 Solidity 安全问题不容忽视

1. 未检查的外部调用

在 2018 年 Solidity 十大安全问题榜单上未检查的外部调用是第三个常见问题。由于现在前两个解决了, 因此未检查的外部调用成为了 2020 年更新列表中最常见的问题。

Solidity 底层调用方法,(例如 address.call()) 不会抛出异常。而是在遇到错误,返回 false

而如果使用合约调用 ExternalContract.doSomething() 时,如果 doSomething() 抛出异常,则异常会继续「冒泡」传播。

应该通过检查返回值来显式处理不成功的情况,以下使用 addr.send() 进行以太币转账是一个很好的例子,这对于其他外部调用也有效。

    if(!addr.send(1)) { 
      revert()
    }

2. 高成本循环

高成本循环从 Solidity 安全榜单的第四名上升至第二名。受该问题影响的智能合约数量增长了近 30%。

大家都知道,以太坊上的运算是需要付费的。因此,减少完成操作所需的计算,不仅仅是优化问题(效率),还涉及到成本费用。

循环是一个昂贵的操作,这里有一个很好的例子:数组中包含的元素越多,就需要更多迭代才能完成循环。最终,无限循环会耗尽所有可用 GAS。

    for(uint256 i=0; i< elements.length; i++) { 
        // do something
    }

如果攻击者能够影响元素数组的长度,则上述代码将导致拒绝服务 (执行无法跳出循环)。而在扫描的智能合约中发现有 8%的合约存在数组长度操纵问题。

3. 权力过大的所有者

这是 Soldiity 十大安全问题新出现的问题,该问题影响了约 16%的合约,某些合约与其所有者(Owner)紧密相关,某些函数只能由所有者地址调用, 如下例所示:

智能合约开发必读:这 10 个 Solidity 安全问题不容忽视

只有合约所有者能够调用 doSomething()doSomethingElse() 函数:前者使用 onlyOwner 修饰器, 而后者则显式执行该修饰器。这带来了严重的风险:如果所有者的私钥遭到泄露, 则攻击者可以控制该合约。

4. 算术精度问题

由于使用 256 位虚拟机(EVM7),Solidity 的数据类型有些复杂。Solidity 不提供浮点运算, 并且少于 32 个字节的数据类型将被打包到同一个 32 字节的槽位中。考虑到这一点,你应该预见以下程序精度问题:

    function calculateBonus(uint amount) returns (uint) { 
        return amount/DELIMITER*BONUS;
    }

如上例所示,在乘法之前执行的除法,可能会有巨大的舍入误差。

5. 依赖 tx.origin

智能合约不应依赖于 tx.origin 进行身份验证,因为恶意合约可能会进行中间人攻击,耗尽所有资金。建议改用 msg.sender

    function transferTo(address dest, uint amount) {    
          require(tx.origin == owner) {       
                   dest.transfer(amount);   
          }
    }

可以在 Solidity 的文档中找到 Tx Origin 攻击的详细说明 [8] 。简单的说,tx.origin 始终是合约调用链中的最初的发起者帐户,而 msg.sender 则表示直接调用者。如果链中的最后一个 合约依赖于 tx.origin 进行身份验证,那么调用链中间环节的合约将能够榨干被调用合约的资金,因为身份验证没有检查究竟是谁(msg.sender)进行了调用。

6. 溢出(Overflow / Underflow)

Solidity 的 256 位虚拟机存在上溢出和下溢出问题(译者注:由于结果超出取值范围称为溢出), 这里 [9] 有具体的分析。在 for 循环条件中使用 uint 数据类型时,开发人员要格外小心,因为它可能导致无限循环:

    for (uint i = border; i >= 0; i--) {  
          ans += i;
    }

在上面的示例中,当 i 的值为 0 时,下一个值为 2^256 -1,这使条件始终为 true。开发人员应当尽量使用 <>!=== 进行比较。

7. 不安全的类型推导

该问题在 Solidity 十大安全问题排行榜中上升了两位,现在影响到的智能合约比之前多了 17%以上。

Solidity 支持类型推导,但有一些奇怪的表现。例如,字面量 0 会被推断为 byte 类型, 而不是通常期望的整型。

在下面的示例中,i 的类型被推断为 uint8,因为这时能够存储 i 的值 uint8 就足够。但如果 elements 数组包含 256 个以上的元素,则下面的代码就会发生溢出:

    for (var i = 0; i < elements.length; i++) { 
       // to something 
    }

建议明确声明数据类型,以避免意外的行为和 / 或错误。

译者注:在 Solidity 0.6 已经移除了 var 定义变量( Solidity 0.6 之后不再有类型推导了),如果使用新的编译器,将不是问题。

8. 不正确的转账

此问题在 Solidity 十大安全问题榜单中从第六位下降到第八位,目前影响不到 1%的智能合约。

在合约之间进行以太币转账有多种方法。虽然官方推荐使用 addr.transfer(x) 函数,但我们仍然找到了还在使用 send() 函数的智能合约:

    if(!addr.send(1)) {    
       revert()
    }

请注意,如果转账不成功,则 addr.transfer(x) 会自动引发异常,同样减轻第一个未检查外部调用的问题

9. 循环内转帐

当在循环体中进行以太币转账时,如果其中一个转账失败(例如,一个合约不能接收),那么整个交易将被回滚。

    for (uint i = 0; i < users.lenghth; i++) { 
       users[i].transfer(amount);
    }

在这个例子中,攻击者可能利用此行为来进行拒绝服务攻击,从而阻止其他用户接收以太币。

10. 时间戳依赖

在 2018 年,时间戳依赖问题排名第五,重要的是要记住,智能合约在不同时刻多个节点上运行的。以太坊虚拟机(EVM)不提供时钟时间,并且通常用于获取时间戳的 now 变量(block.timestamp 的别名)实际上是矿工可以操纵的环境变量。

    if (timeHasCome == block.timestamp) {    
        winner.transfer(amount);
     }

由于矿工可以操纵当前的环境变量,因此只能在不等式 ><>=<= 中使用其值。

如果你的应用需要随机性,可以参考 RANDAO 合约 [10], 该合约基于任何人都可以参与的去中心化自治组织(DAO),是所有参与者共同生成的随机数。

总结

比较 2018 年和 2020 年十大常见问题时,我们可以观察到开发最佳实践的一些进展,尤其是那些影响安全性的实践。看到 2018 年排名前 2 位的问题:外部合约拒绝服务重入,已经不再榜单了,这是一个积极的信号,但仍然需要采取措施来避免这类常见错误。

请记住,智能合约在设计上是不可变的,这意味着一旦创建,就无法修补源代码。这对安全性构成了巨大挑战,开发人员应利用可用的安全测试工具来确保在部署之前对源代码进行了充分的测试和审核。

Solidity 是一种非常新且仍在成熟的编程语言, Solidity v0.6.0 引入了一些重大更改 [11],并且预计在以后的版本中还会有更多更改。

来源链接:securityboulevard.com