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)緊密相關,某些函式只能由所有者地址呼叫, 如下例所示:
只有合約所有者能夠呼叫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],並且預計在以後的版本中還會有更多更改。
原文作者:Erez Yalon, Erez Yalon領導Checkmarx安全研究小組。他擁有豐富的防禦者和攻擊者經驗,並且是一名獨立的安全研究人員。