智能合約開發必讀:這10個Solidity安全問題不容忽視
編者按:本文來自登鏈社區,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. 循環內轉帳
總結







