智能合約開發必讀:這10個Solidity安全問題不容忽視

登链社区
本文约3645字,阅读全文需要约15分钟
智能合約安全堪憂,了解2020 年Solidity 常見的10 個安全問題。

編者按:本文來自登鏈社區,Odaily經授權轉載。

編者按:本文來自

登鏈社區

登鏈社區

,Odaily經授權轉載。

編者按:本文來自

登鏈社區

登鏈社區

,Odaily經授權轉載。

編者按:本文來自

登鏈社區

登鏈社區

,Odaily經授權轉載。

在2018 年,我們(CheckMarx)曾對智能合約安全狀況進行過初步研究,重點是Solidity[1] 編寫的智能合約。當時,我們根據公開的合約源代碼(譯者註:本文稱之為已掃描合約,本文出現的x% 是以此為基數)編寫了最常見的10 個智能合約安全問題。兩年過去了該更新研究並評估智能合約安全性發展的如何了。

值得關注的其他問題

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

儘管有一個安全問題排名很不錯,但它往往一些有趣的細節,因為某些細節與排名列表並不完全一致。在深入挖掘10 大問題之前,必要闡述一下原始研究中一些值得關注的亮點問題:

在2018 年,最主要的兩個問題是外部合約拒絕服務和重入。但是現在這些問題有所緩解(不過依舊值得關注)。可以從我們的研究博客中了解更多有關Reentrancy 的信息:從安全角度出發審視智能合約[2]。

譯者註:實際上由於DeFi 應用之間的組合應用(例如閃電貸),又導致了多起嚴重的重入攻擊事件。

現在Solidity v0.6.x 發布[3] 了,它帶來了許多重大變化[4],然而掃描的智能合約中有50%甚至還沒有準備好使用Solidity v0.5.0 編譯器。另外30% 智能合約使用了過時的語法(例如:使用sha3、throw 、constant 等),並且83%的合約在指定編譯器版本存在規範問題(pragma)。

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

譯者註:Solidity 0.6 在語義上更明確了(例如0.6 版本在繼承方面的升級[5]),有助於編譯器及時發現問題,讓代碼更安全,

儘管可見性問題[6] 沒有出現在2018 年的前10 位,也沒有出現今年的前10,但可見性問題增加了48%,值得關注。

下表比較了2018 年和2020 年十大常見問題列表之間的變化。這些問題按嚴重程度和流行程度排序:

1. 未檢查的外部調用

在2018 年Solidity 十大安全問題榜單上未檢查的外部調用是第三個常見問題。由於現在前兩個解決了, 因此未檢查的外部調用成為了2020 年更新列表中最常見的問題。

Solidity 底層調用方法,(例如address.call()) 不會拋出異常。而是在遇到錯誤,返回false。

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

而如果使用合約調用ExternalContract.doSomething() 時,如果doSomething() 拋出異常,則異常會繼續「冒泡」傳播。

應該通過檢查返回值來顯式處理不成功的情況,以下使用addr.send() 進行以太幣轉賬是一個很好的例子,這對於其他外部調用也有效。

2. 高成本循環

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

高成本循環從Solidity 安全榜單的第四名上升至第二名。受該問題影響的智能合約數量增長了近30%。

大家都知道,以太坊上的運算是需要付費的。因此,減少完成操作所需的計算,不僅僅是優化問題(效率),還涉及到成本費用。

循環是一個昂貴的操作,這裡有一個很好的例子:數組中包含的元素越多,就需要更多迭代才能完成循環。最終,無限循環會耗盡所有可用GAS。

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

如果攻擊者能夠影響元素數組的長度,則上述代碼將導致拒絕服務(執行無法跳出循環)。而在掃描的智能合約中發現有8%的合約存在數組長度操縱問題。<、>這是Soldiity 十大安全問題新出現的問題,該問題影響了約16%的合約,某些合約與其所有者(Owner)緊密相關,某些函數只能由所有者地址調用, 如下例所示:

6. 溢出(Overflow / Underflow)

只有合約所有者能夠調用doSomething() 和doSomethingElse() 函數:前者使用onlyOwner修飾器, 而後者則顯式執行該修飾器。這帶來了嚴重的風險:如果所有者的私鑰遭到洩露, 則攻擊者可以控制該合約。

4. 算術精度問題

由於使用256 位虛擬機(EVM[7]),Solidity 的數據類型有些複雜。 Solidity 不提供浮點運算, 並且少於32 個字節的數據類型將被打包到同一個32 字節的槽位中。考慮到這一點,你應該預見以下程序精度問題:

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

如上例所示,在乘法之前執行的除法,可能會有巨大的捨入誤差。

5. 依賴tx.origin

智能合約不應依賴於tx.origin 進行身份驗證,因為惡意合約可能會進行中間人攻擊,耗盡所有資金。建議改用msg.sender:

可以在Solidity 的文檔中找到Tx Origin 攻擊的詳細說明[8] 。簡單的說,tx.origin 始終是合約調用鏈中的最初的發起者帳戶,而msg.sender 則表示直接調用者。如果鏈中的最後一個合約依賴於tx.origin 進行身份驗證,那麼調用鏈中間環節的合約將能夠榨乾被調用合約的資金,因為身份驗證沒有檢查究竟是誰(msg.sender)進行了調用。

6. 溢出(Overflow / Underflow)

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

Solidity 的256 位虛擬機存在上溢出和下溢出問題(譯者註:由於結果超出取值範圍稱為溢出), 這裡[9] 有具體的分析。在for 循環條件中使用uint 數據類型時,開發人員要格外小心,因為它可能導致無限循環:

在上面的示例中,當i 的值為0 時,下一個值為2^256 -1,這使條件始終為true。開發人員應當盡量使用

、!= 和== 進行比較。

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

7. 不安全的類型推導

該問題在Solidity 十大安全問題排行榜中上升了兩位,現在影響到的智能合約比之前多了17%以上。

Solidity 支持類型推導,但有一些奇怪的表現。例如,字面量0 會被推斷為byte 類型, 而不是通常期望的整型。

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

在下面的示例中,i 的類型被推斷為uint8,因為這時能夠存儲i 的值uint8 就足夠。但如果elements 數組包含256 個以上的元素,則下面的代碼就會發生溢出:<、>建議明確聲明數據類型,以避免意外的行為和/ 或錯誤。

8. 不正確的轉賬

總結

此問題在Solidity 十大安全問題榜單中從第六位下降到第八位,目前影響不到1%的智能合約。

9. 循環內轉帳

總結

來源鏈接:securityboulevard.com