OWASP Smart Contract Top 10

隨著區塊鏈和智能合約的興起,OWASP 也發布了 「Smart Contract Top 10」,這份清單針對智能合約特有的安全漏洞進行了總結。這份清單是開發人員、審計師和安全專業人士的重要參考,幫助他們優先處理最關鍵的風險。

OWASP(開放式網頁應用程式安全專案)是一個非營利組織,致力於提升軟體安全性。他們最著名的就是「OWASP Top 10」,這份清單列出了當前最嚴重的網頁應用程式安全風險。


OWASP Smart Contract Top 10 (2025)

  1. SC01:2025 – 存取控制不當 (Improper Access Control)
    • 簡介:這是最常見也最嚴重的漏洞之一。當智能合約沒有正確限制誰可以呼叫特定功能時,攻擊者就能未經授權地執行關鍵操作,例如轉移資金或更改合約設定。
  2. SC02:2025 – 價格預言機操縱(Price Oracle Manipulation)
    • 簡介:許多去中心化金融(DeFi)協議依賴外部數據源(稱為「預言機」)來獲取資產價格。攻擊者可以透過操縱這些價格數據,人為地改變資產價值,從而觸發錯誤的清算或套利行為。
  3. SC03:2025 – 邏輯錯誤 (Logic Errors)
    • 簡介:這是指智能合約的業務邏輯存在缺陷。即使程式碼沒有語法錯誤,但設計上的缺陷仍可能導致合約行為與預期不符,從而造成資金損失或其他意外後果。
  4. SC04:2025 – 缺乏輸入驗證 (Lack of Input Validation)
    • 簡介:如果智能合約沒有對使用者輸入的參數進行嚴格檢查,攻擊者可以傳入惡意或意外的數據,從而觸發非預期的行為,例如整數溢出或重入攻擊。
  5. SC05:2025 – 重入攻擊 (Reentrancy)
    • 簡介:這是一種經典的區塊鏈攻擊手法。當智能合約在尚未完成自己的狀態更新前,就呼叫外部合約,攻擊者可以在此期間重新進入原合約,反覆執行同一函數,從而耗盡合約中的資金。
  6. SC06:2025 – 未檢查的外部調用 (Unchecked External Calls)
    • 簡介:智能合約經常需要呼叫其他合約的函數。如果沒有檢查外部呼叫的返回值,即使呼叫失敗,合約也會繼續執行,這可能導致合約狀態不一致或安全漏洞。
  7. SC07:2025 – 閃電貸款攻擊 (Flash Loan Attacks)
    • 簡介:閃電貸允許用戶在沒有抵押品的情況下,借出大量資金並在單一交易中償還。攻擊者利用這個特性,藉由大筆資金來操縱市場,然後在還款前從中獲利。
  8. SC08:2025 – 整數溢位與下溢 (Integer Overflow and Underflow)
    • 簡介:當數字運算超出資料類型所能儲存的最大值或最小值時,就會發生溢出或下溢。這會導致數字「繞回」,例如 uint8 的 255 + 1 會變成 0,這會讓攻擊者能操縱餘額。
  9. SC09:2025 – 不安全的隨機數生成 (Insecure Randomness)
    • 簡介:區塊鏈本質上是確定性的,因此很難生成真正的隨機數。如果合約使用區塊時間戳或區塊哈希等可預測的變數來生成隨機數,攻擊者就可以預測或操縱結果,從而影響博弈、抽獎等合約的公平性。
  10. SC10:2025 – 拒絕服務攻擊 (Denial of Service, DoS)
    • 簡介:DoS 攻擊旨在透過耗盡 Gas 費或阻斷關鍵功能,讓智能合約無法正常運作。這會導致合法用戶無法與合約互動,從而影響其可用性。

這份清單為智能合約的安全性提供了一個重要的參考標準,開發人員應在設計和編碼過程中特別留意這些潛在的風險。


Improper Access Control

存取控制不當是一種智能合約中的安全漏洞,會導致未經授權的使用者能夠存取或修改合約中的資料或功能。這種漏洞通常是因為合約的程式碼未根據使用者的權限等級適當限制存取權限所引起的。在智能合約中,存取控制涉及治理和關鍵邏輯,例如:

  • 發行(鑄造)代幣
  • 對提案進行投票
  • 提取資金
  • 暫停或升級合約
  • 更改合約擁有權

如果存取控制不足,任何人都可能呼叫敏感功能,從而引發嚴重的安全問題。

範例(漏洞合約)

以下是一個存在存取控制漏洞的智能合約範例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Solidity_AccessControl {
    mapping(address => uint256) public balances;

    // Burn function with no access control
    function burn(address account, uint256 amount) public {
        _burn(account, amount);
    }
}

問題分析

  • 在這個合約中,burn 函數被標記為 public,這意味著任何人都可以呼叫它,無需任何權限檢查。
  • 這允許未經授權的使用者銷毀(burn)任意帳戶的代幣餘額,嚴重影響合約的安全性和完整性。


Price Oracle Manipulation

在去中心化金融(DeFi)中,預言機(oracles)扮演著重要角色,它們是連接區塊鏈內智能合約和區塊鏈外現實世界資料的橋樑。當智能合約需要知道某個資產的市場價格時,它會向預言機請求資料。然而,如果攻擊者能夠操縱預言機提供的資料,就會導致合約做出錯誤的判斷,進而引發一系列災難性後果。

攻擊者可能會通過以下方式利用這個漏洞:

  • 操縱預言機提供的價格資料,讓智能合約誤以為某個資產的價值發生了改變。
  • 利用這些被操縱的價格,進行未經授權的借貸、超額槓桿交易,甚至耗盡流動性池。

漏洞範例

以下是一個存在漏洞的智能合約範例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IPriceFeed {
    function getLatestPrice() external view returns (int);
}

contract PriceOracleManipulation {
    address public owner;
    IPriceFeed public priceFeed;

    constructor(address _priceFeed) {
        owner = msg.sender;
        priceFeed = IPriceFeed(_priceFeed);
    }

    function borrow(uint256 amount) public {
        int price = priceFeed.getLatestPrice();  
        require(price > 0, "Price must be positive");

        // Vulnerability: No validation or protection against price manipulation
        uint256 collateralValue = uint256(price) * amount;

        // Borrow logic based on manipulated price
        // If an attacker manipulates the oracle, they could borrow more than they should
    }
    // Repayment logic
}

漏洞分析

這個合約的 borrow 函數直接使用從預言機 priceFeed 獲取的價格,但沒有任何機制來驗證這個價格的真實性或合理性。如果攻擊者能操控 priceFeed,將資產價格虛報,他們就能以較少的抵押品借出更多的資金。


Logic Errors

邏輯錯誤(Logic Errors),也稱為業務邏輯漏洞,是智能合約中的一種隱性缺陷。這類錯誤發生在合約的程式碼邏輯與其預期行為不一致時。邏輯錯誤可能以多種形式出現,例如:

  • 獎勵分配錯誤:在分配獎勵時計算錯誤,導致不公平的分配。
  • 不當的代幣生成:未受控制或錯誤的代幣生成邏輯,允許無限或非預期的代幣生成。
  • 借貸池不平衡:存款和提款追蹤錯誤,導致資金池儲備不一致。

這些漏洞通常隱藏在合約的邏輯中,難以察覺,需仔細檢查才能發現。

範例(存在漏洞的合約)

以下是一個存在邏輯錯誤的智能合約範例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Solidity_LogicErrors {
    mapping(address => uint256) public userBalances;
    uint256 public totalLendingPool;

    function deposit() public payable {
        userBalances[msg.sender] += msg.value;
        totalLendingPool += msg.value;
    }

    function withdraw(uint256 amount) public {
        require(userBalances[msg.sender] >= amount, "Insufficient balance");

        // Faulty calculation: Incorrectly reducing the user's balance without updating the total lending pool
        userBalances[msg.sender] -= amount;

        payable(msg.sender).transfer(amount);
    }

    function mintReward(address to, uint256 rewardAmount) public {
        // Faulty minting logic: Reward amount not validated
        userBalances[to] += rewardAmount;
    }
}

漏洞分析

  1. 提款功能:在 withdraw 函數中,僅更新了用戶的餘額 (userBalances),但未同步減少總資金池 (totalLendingPool)。這導致資金池的總額與實際餘額不符,可能造成資金池顯示的資金高於實際可用資金。
  2. 獎勵生成功能mintReward 函數未對 rewardAmount 進行任何驗證,可能允許生成任意數量的代幣,導致代幣供應膨脹。


Lack of Input Validation

輸入驗證(Input Validation)是確保智能合約僅處理有效且預期的資料。當智能合約未對輸入資料進行驗證時,可能會暴露於多種安全風險,例如邏輯操控、未授權存取或意外行為。如果合約假設用戶輸入的資料總是有效而未進行檢查,攻擊者可能利用這一點輸入惡意資料,從而危害合約的安全性和可靠性。

範例(存在漏洞的合約)

以下是一個缺乏輸入驗證的智能合約範例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Solidity_LackOfInputValidation {
    mapping(address => uint256) public balances;

    function setBalance(address user, uint256 amount) public {
        // The function allows anyone to set arbitrary balances for any user without validation.
        balances[user] = amount;
    }
}

漏洞分析

  • setBalance 函數:該函數允許任何人為任何地址(user)設置任意的餘額(amount),且未對輸入進行任何驗證。
  • 問題
    • 沒有檢查 user 是否為有效地址(例如,是否為零地址)。
    • 沒有限制誰可以調用該函數,任何人都能修改任何地址的餘額。
    • 沒有對 amount 進行範圍或有效性檢查,可能導致不合理的餘額值。

這使得攻擊者可以輕易操控合約狀態,例如將自己的餘額設置為任意大值,或將其他用戶的餘額清零。


Reentrancy

重入攻擊(Reentrancy Attack)是一種智能合約中的漏洞,當合約在更新自身狀態(如餘額)之前,對外部合約進行調用時,攻擊者可能利用這一點重複調用原始函數,從而執行不當操作,例如多次提領資金。這種攻擊通常發生在合約向外部地址發送以太幣(Ether)或調用外部合約時,攻擊者可通過惡意合約重新進入(re-enter)原始合約,利用未更新的狀態進行惡意操作,最終可能耗盡合約中的所有資金。

範例(存在漏洞的合約)

以下是一個易受重入攻擊的智能合約範例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Solidity_Reentrancy {
    mapping(address => uint) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint amount = balances[msg.sender];
        require(amount > 0, "Insufficient balance");

        // Vulnerability: Ether is sent before updating the user's balance, allowing reentrancy.
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        // Update balance after sending Ether
        balances[msg.sender] = 0;
    }
}

漏洞分析

  • withdraw 函數
    • 在發送以太幣(msg.sender.call)後才將用戶餘額設置為 0。
    • 如果 msg.sender 是一個惡意合約,該合約可以在接收以太幣時立即重新調用 withdraw 函數。
    • 因為餘額尚未更新(仍為非零),攻擊者可以重複提領資金,直到耗盡合約的以太幣。
  • 問題:這種「先發送、後更新」的邏輯允許攻擊者在狀態更新前多次執行提領操作。


Unchecked External Calls

未檢查的外部調用(Unchecked External Calls)是一種安全漏洞,指合約在對另一個合約或地址進行外部調用時,未正確檢查調用的結果。在以太坊中,當一個合約調用另一個合約時,被調用的合約可能會靜默失敗而不拋出異常。如果調用合約未檢查返回值的話,可能會錯誤地假設調用成功,即使實際上並非如此。這可能導致合約狀態不一致,並成為攻擊者可利用的漏洞。

範例(存在漏洞的合約)

以下是一個存在未檢查外部調用漏洞的智能合約範例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Solidity_UncheckedExternalCall {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function forward(address callee, bytes memory _data) public {
        callee.delegatecall(_data);
    }
}

漏洞分析

  • forward 函數:該函數使用 delegatecall 對用戶提供的地址 (callee) 進行調用,但未檢查調用的返回結果。
  • 問題
    • 如果 delegatecall 失敗,合約不會察覺,並繼續執行,導致狀態可能不一致。
    • 此外,該函數允許任意地址執行任意程式碼,且未進行驗證,這增加了額外風險(詳見注意事項)。

Flash Loan Attacks

閃電貸款攻擊(Flash Loan Attacks)是一種利用區塊鏈閃電貸款機制的攻擊方式。閃電貸款允許用戶在單一交易內無需抵押即可借入大額資金,但前提是必須在同一交易內償還貸款(由於區塊鏈交易的原子性,所有操作要麼全部成功,要麼全部失敗)。攻擊者利用這一點,結合其他漏洞(如價格預言機操控、重入攻擊或邏輯錯誤),操縱合約行為,從而竊取資金或造成其他損害。

閃電貸款攻擊的運作原理

閃電貸款的特點:

  • 無需抵押:用戶可借入巨額資金(例如,數百萬美元的代幣),只要在交易結束前償還。
  • 原子性:所有操作(借貸、操作、償還)在同一交易內完成,無需等待區塊確認。
  • 低成本:閃電貸款費用通常很低(例如,Aave 收取 0.09% 的費用),使攻擊成本低廉。

攻擊者利用閃電貸款借入大量資金,然後在單一交易內執行以下操作:

  1. 操縱市場或合約狀態(例如,價格預言機或流動性池)。
  2. 利用目標合約的漏洞(如邏輯錯誤或未驗證的輸入)獲利。
  3. 償還閃電貸款,保留非法所得。

閃電貸款攻擊的範例

以下是常見的閃電貸款攻擊類型:

價格預言機操控(Oracle Manipulation)

  • 情境:許多去中心化金融(DeFi)協議依賴價格預言機(如 Chainlink 或單一交易所的價格)來決定資產價值或清算條件。
  • 範例:2020 年的 bZx 攻擊,攻擊者操縱價格預言機,導致協議誤以為抵押品不足,從而清算資產。

流動性池耗盡(Liquidity Pool Draining)

  • 情境:自動做市商(AMM)如 Uniswap 或 SushiSwap 使用流動性池來提供交易流動性。
  • 範例:某些早期 AMM 協議因未限制大額交易而被攻擊。流動性提供者(LPs)原本存放在池子裡的資產,會因為攻擊者的行為而大量流失,最終只剩下價值不高的代幣,蒙受巨大的財務損失。由於流動性池資金不足,其他用戶也無法正常進行交易或提款,嚴重影響了協議的運作。

套利攻擊(Arbitrage Exploits)

  • 情境:不同交易所之間的代幣價格存在差異。
  • 範例:利用 DEX 價格差異進行套利,結合漏洞放大收益。

Integer Overflow and Underflow

整數溢位(Overflow)與下溢(Underflow)是智能合約中的一種安全漏洞,源於以太坊虛擬機(EVM)中整數資料型別的固定大小限制。EVM 使用固定長度的整數型別,例如:

  • uint8(無符號 8 位整數):只能表示 0 到 255 的數字。
  • int8(有符號 8 位整數):可表示 -128 到 127 的數字。

當算術運算結果超出型別的範圍時,會發生溢位或下溢:

  • 溢位(Overflow):試圖將值設為超出型別最大範圍。例如,將 uint8 的值 255 加 1,結果不是 256,而是「回繞」到 0。
  • 下溢(Underflow):試圖將值減到小於型別最小範圍。例如,將 uint8 的值 0 減 1,結果不是 -1,而是「回繞」到 255。
  • 有符號整數:對於 int8,如果從 -128 減 1,結果會回繞到 127。

這種行為類似於汽車里程表(達到 999999 後重置為 000000)或週期性數學函數(如正弦函數)。

範例(存在漏洞的合約)

以下是一個易受整數溢位和下溢攻擊的合約範例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.4.17;

contract Solidity_OverflowUnderflow {
    uint8 public balance;

    constructor() public {
        balance = 255; // Maximum value of uint8
    }

    // Increments the balance by a given value
    function increment(uint8 value) public {
        balance += value; // Vulnerable to overflow
    }

    // Decrements the balance by a given value
    function decrement(uint8 value) public {
        balance -= value; // Vulnerable to underflow
    }
}

漏洞分析

  • 溢位漏洞:假設 balance = 255,調用 increment(1) 會導致 255 + 1 = 0(溢位),因為 uint8 最大值為 255,超過後回繞到 0。
  • 下溢漏洞:假設 balance = 0,調用 decrement(1) 會導致 0 - 1 = 255(下溢)。
  • 攻擊情境
    • 攻擊者可調用 increment(1)balance 從 255 變為 0,造成餘額錯誤重置。
    • 攻擊者可調用 decrement(1) 從 0 變為 255,製造大量虛假餘額,進而提領不屬於自己的資金。


Insecure Randomness

隨機數生成在諸如賭博、遊戲贏家選擇或隨機種子生成等應用中至關重要。然而,在以太坊上生成隨機數是一項挑戰,因為以太坊的區塊鏈是確定性的(Deterministic),即所有節點必須對同一輸入產生相同結果,這使得生成真正的隨機數幾乎不可能。因此,Solidity 通常依賴偽隨機(Pseudorandom)因素來模擬隨機性。此外,Solidity 中的複雜計算會消耗大量 gas,增加了設計隨機數生成器的成本。

開發者常使用區塊鏈相關資訊來生成隨機數,但這些方法不安全,因為它們可能被礦工(Miners)或攻擊者操縱。這些來源容易被礦工操縱(例如,調整時間戳或選擇有利區塊),從而影響合約的隨機數生成結果。

範例(存在漏洞的合約)

以下是一個使用不安全隨機數生成的合約範例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Solidity_InsecureRandomness {
    constructor() payable {}

    function guess(uint256 _guess) public {
        uint256 answer = uint256(
            keccak256(
                abi.encodePacked(block.timestamp, block.difficulty, msg.sender) // Using insecure mechanisms for random number generation
            )
        );

        if (_guess == answer) {
            (bool sent,) = msg.sender.call{value: 1 ether}("");
            require(sent, "Failed to send Ether");
        }
    }
}

漏洞分析

  • guess 函數:使用 block.timestampblock.difficultymsg.sender 結合 keccak256 來生成隨機數。
  • 問題
    • 礦工可以操縱 block.timestamp(在合理範圍內調整時間戳)或 block.difficulty,影響 keccak256 的輸出。
    • 攻擊者可能預測或通過試驗推導出隨機數(特別是在區塊數據公開後)。
    • 這種隨機數生成方式並非真正的隨機,容易被操控,導致攻擊者能以高機率猜中 answer


Denial of Service

拒絕服務攻擊(Denial of Service, DoS)在 Solidity 智能合約中是指攻擊者通過利用合約漏洞,耗盡資源(如 gas、CPU 週期或儲存空間),使合約無法正常運作,從而阻止用戶與合約交互。這些攻擊的目標是使合約不可用,影響其功能、用戶體驗及相關去中心化應用(dApp)的運作。

範例(存在漏洞的合約)

以下是一個易受 DoS 攻擊的智能合約範例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Solidity_DOS {
    address public king;
    uint256 public balance;

    function claimThrone() external payable {
        require(msg.value > balance, "Need to pay more to become the king");

        //If the current king has a malicious fallback function that reverts, it will prevent the new king from claiming the throne, causing a Denial of Service.
        (bool sent,) = king.call{value: balance}("");
        require(sent, "Failed to send Ether");

        balance = msg.value;
        king = msg.sender;
    }
}

合約功能

  • 這是一個「國王遊戲」合約,允許用戶通過支付比當前 balance 更高的金額(msg.value)來成為新的「國王」(king)。
  • 當新用戶成功支付後,舊的 king 會收到退款(當前的 balance),然後 kingbalance 更新為新用戶的地址和支付金額。

漏洞分析

  • 問題:在 claimThrone 函數中,合約使用 king.call{value: balance}("") 向舊的 king 發送以太幣。
    • 如果舊的 king 是一個惡意合約,且其回退函數(receivefallback)故意回滾(例如,通過 revert 或無限迴圈耗盡 gas),則 call 會失敗,導致 require(sent, "Failed to send Ether") 觸發交易回滾。
    • 這意味著新用戶無法成為 king,因為舊 king 的惡意行為阻止了合約狀態的更新。
  • DoS 風險:惡意 king 可以持續阻止任何新用戶更新 king,使合約無法正常運作。