2025年6月10日 星期二

什麼是 SQL Injection?如何防範?

 SQL Injection(SQL 注入)是一種常見且危險的網路應用程式安全漏洞。它利用了應用程式在處理使用者輸入時的疏忽,讓惡意使用者能夠在應用程式的 SQL 查詢中插入惡意的 SQL 程式碼,從而操縱資料庫,達到未經授權的存取、修改、刪除甚至控制整個資料庫的目的。


什麼是 SQL Injection?

SQL Injection 的核心原理是:應用程式將不可信的使用者輸入直接或間接地拼接到 SQL 查詢字串中,而沒有進行適當的驗證或轉義。

這會導致資料庫將使用者輸入的惡意字串視為 SQL 語句的一部分來執行,而不是單純的資料。

舉例說明:

假設你的網站有一個登入功能,後端程式碼可能是這樣構建 SQL 查詢的:

PHP
// PHP 範例
$username = $_POST['username']; // 從使用者輸入獲取用戶名
$password = $_POST['password']; // 從使用者輸入獲取密碼

$sql = "SELECT * FROM users WHERE username = '" . $username . "' AND password = '" . $password . "'";

// 執行這個 SQL 查詢...

如果使用者在 username 欄位輸入 admin,在 password 欄位輸入 mypass,則生成的 SQL 查詢會是:

SQL
SELECT * FROM users WHERE username = 'admin' AND password = 'mypass';

這看起來沒問題。但如果惡意使用者在 username 欄位輸入 admin' OR '1'='1,在 password 欄位隨意輸入(例如 whatever),那麼生成的 SQL 查詢將會是:

SQL
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'whatever';

請注意 admin' OR '1'='1 如何改變了查詢的結構:

  1. 第一個單引號 ' 關閉了 username 的字串。
  2. OR '1'='1' 是一個永遠為真的條件。
  3. 後面的 -- (在某些資料庫中是 #/*...*/) 是 SQL 的註解符號,它會將查詢中剩餘的部分(例如 AND password = 'whatever')變成註解,從而使其失效。

現在,無論 password 是什麼,OR '1'='1' 這個條件都為真,導致資料庫會返回 users 表中的所有行(或者第一行符合 admin 的使用者),讓攻擊者繞過密碼驗證成功登入。

更具破壞性的攻擊:

攻擊者還可以利用 SQL Injection 執行更危險的操作:

  • 竊取資料: SELECT * FROM users WHERE id = '1' UNION SELECT username, password FROM admins; --
  • 刪除資料: DELETE FROM products WHERE id = '1'; DROP TABLE users; --
  • 修改資料: UPDATE products SET price = 0 WHERE id = '1'; --
  • 執行系統命令 (如果資料庫和權限允許): EXEC xp_cmdshell 'dir c:\'; -- (針對 SQL Server)

如何防範 SQL Injection?

防範 SQL Injection 的最有效方法是永遠不要將使用者輸入直接拼接或構造到 SQL 查詢中。以下是主要的防範策略:

1. 使用參數化查詢 (Parameterized Queries) 或預處理語句 (Prepared Statements) - 最推薦且最有效

這是防範 SQL Injection 的黃金法則。它將 SQL 程式碼和使用者輸入的資料完全分離。你為 SQL 查詢中的每個變數創建一個佔位符,然後將使用者輸入作為參數綁定到這些佔位符上。

原理: 資料庫會先解析帶有佔位符的 SQL 語句,然後再將參數的值安全地傳遞給它。資料庫會將這些參數視為純粹的資料,而不是可執行的程式碼。即使參數中包含 SQL 關鍵字或攻擊字串,它們也只會被當作字串值處理。

PHP 範例 (使用 PDO):

PHP
// 連接到資料庫
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'username', 'password');

// 準備 SQL 語句,使用 ? 作為佔位符
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");

// 綁定參數。資料庫會將這些值視為字串,即使它們包含惡意字元。
$stmt->bindParam(1, $username); // 這裡的 $username 是使用者輸入
$stmt->bindParam(2, $password); // 這裡的 $password 是使用者輸入

// 執行語句
$stmt->execute();

// 獲取結果
$user = $stmt->fetch(PDO::FETCH_ASSOC);

if ($user) {
    echo "登入成功!";
} else {
    echo "登入失敗。";
}

2. 使用 ORM (Object-Relational Mapping) 框架

諸如 Laravel 的 Eloquent ORM、Doctrine (PHP)、Hibernate (Java)、SQLAlchemy (Python) 等現代 ORM 框架,底層通常都使用參數化查詢來與資料庫互動,因此它們能天然地防範 SQL Injection。

Laravel Eloquent 範例:

PHP
// 假設 User 是 Eloquent Model
$username = $request->input('username');
$password = $request->input('password');

// Eloquent 會自動使用參數化查詢
$user = User::where('username', $username)
            ->where('password', $password)
            ->first();

if ($user) {
    // 登入成功
} else {
    // 登入失敗
}

3. 輸入驗證 (Input Validation) 和清理 (Sanitization)

雖然參數化查詢是主要防禦手段,但良好的輸入驗證和清理仍然是重要的防禦深度。

  • 驗證 (Validation):
    • 白名單驗證 (Whitelist Validation): 只允許已知的、安全的輸入。例如,如果一個欄位應該是數字,就只允許數字。如果一個欄位應該是電子郵件,就只允許合法的電子郵件格式。這是最推薦的方式。
    • 黑名單驗證 (Blacklist Validation): 禁用已知的惡意字元或模式(例如 ', ;, --, OR 等)。這種方式不夠可靠,因為攻擊者總能找到繞過黑名單的方法。
  • 清理 (Sanitization):
    • 移除或轉義輸入中可能造成危害的特殊字元。例如,將單引號 ' 轉換為 '' (兩個單引號) 或 \' (帶有轉義符的單引號)。
    • 重要: 僅僅轉義輸入並不能完全防範 SQL Injection,因為轉義規則可能因資料庫類型或編碼而異,且容易出錯。參數化查詢才是王道。

4. 最小權限原則 (Principle of Least Privilege)

  • 限制資料庫使用者的權限: 應用程式連接資料庫時使用的帳戶,只應擁有完成其任務所需的最小權限。例如,如果應用程式只需要讀取資料,就不要給予 INSERT, UPDATE, DELETEDROP TABLE 等權限。
  • 避免使用高權限帳戶: 絕不使用資料庫管理員帳戶 (如 root, sa) 來運行應用程式。

5. 關閉錯誤訊息 (Error Messages)

  • 在生產環境中,不要將詳細的資料庫錯誤訊息直接顯示給使用者。這些錯誤訊息可能包含有助於攻擊者理解資料庫結構或漏洞的資訊。
  • 應將錯誤記錄到日誌檔案中,並向使用者顯示一個通用的、友好的錯誤提示。

6. 定期更新和修補

  • 保持你的應用程式框架、資料庫系統、驅動程式和所有相關軟體庫都是最新版本。開發者會不斷發布安全補丁來修復已知漏洞。

7. Web Application Firewall (WAF)

  • WAF 可以在網路層面提供額外的保護。它會檢查進出應用程式的 HTTP 請求,並嘗試識別和阻止惡意流量,包括 SQL Injection 攻擊。雖然 WAF 不能替代程式碼層面的防護,但它可以作為一道額外的防線。

總結

防範 SQL Injection 最核心、最有效的措施是使用參數化查詢或預處理語句。結合輸入驗證、最小權限原則和適當的錯誤處理,可以大大提高應用程式的安全性。

沒有留言:

張貼留言

網誌存檔