智能合約安全堪憂,瞭解 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 位虛擬機(EVM[7]),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