SQL Injection(SQL 注入防範)

什麼是 SQL Injection(What is SQL Injection)

這篇 SQL Injection 教學將帶你認識 SQL 注入攻擊的原理與防範方式。SQL Injection(SQL 注入)是一種常見且危險的網路攻擊手法,攻擊者透過在使用者輸入欄位中插入惡意的 SQL 程式碼,使資料庫執行非預期的操作。

透過 SQL Injection,攻擊者可以繞過登入驗證、竃取或篤改資料庫中的機密資料、刪除整個資料表,甚至控制整台伺服器。由於許多網站和應用程式都依賴 SQL 資料庫,因此理解並防範 SQL Injection 是每位開發者必備的資安知識。

SQL Injection 攻擊原理(How SQL Injection Works)

SQL Injection 的核心原理是:當應用程式將使用者輸入的內容直接拼接到 SQL 查詢字串中,而沒有進行驗證或處理時,攻擊者就可以透過特殊字元來改變 SQL 語句的邏輯。

假設有一個登入表單,後端程式使用以下方式驗證使用者身份:

-- 後端程式直接拼接使用者輸入
SELECT * FROM users
WHERE username = '' + input_username + ''
AND password = '' + input_password + '';

當使用者正常輸入帳號 admin 和密碼 1234 時,SQL 查詢如下:

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

這是正常的查詢,沒有任何問題。但如果攻擊者在帳號欄位輸入以下內容:

-- 攻擊者在帳號欄位輸入:
' OR '1'='1' --

那麼拼接後的 SQL 語句就會變成:

SELECT * FROM users
WHERE username = '' OR '1'='1' --'
AND password = '';

由於 '1'='1' 永遠為 TRUE,而 -- 是 SQL 的註解符號會忽略後面的密碼驗證,因此這個查詢會回傳所有使用者資料,攻擊者就能不需要密碼就成功登入。

常見 SQL Injection 攻擊類型(Common SQL Injection Types)

攻擊類型說明
Classic SQL Injection直接在輸入欄位插入惡意 SQL,立即看到攻擊結果
Blind SQL Injection應用程式不會顯示錯誤訊息,攻擊者透過 TRUE/FALSE 回應推測資料
Union-based Injection利用 UNION 合併額外的 SELECT 查詢來竃取資料
Time-based Injection透過 SQL 延遲函數(如 SLEEP)判斷條件是否成立
Error-based Injection利用資料庫的錯誤訊息來獲取資料庫結構資訊

SQL Injection 攻擊範例(SQL Injection Examples)

繞過登入驗證

假設有一個使用者資料表 users 如下(若需了解如何建立資料表,請參考 CREATE TABLE 教學):

idusernamepasswordrole
1admina1b2c3d4admin
2johnpass123user
3marymary5678user

攻擊者在登入表單的帳號欄位輸入以下內容,密碼欄位隨便輸入:

-- 帳號欄位輸入:
admin' --

-- 產生的 SQL:
SELECT * FROM users
WHERE username = 'admin' --'
AND password = 'anything';

-- 後面的密碼驗證被註解掉,攻擊者直接以 admin 身份登入,不需要知道密碼。

竃取其他資料表的資料

攻擊者可以利用 UNION 合併查詢來竃取其他資料表的內容。假設網站有一個商品查詢功能:

-- 原始查詢(依照商品名稱搜尋)
SELECT name, price FROM products
WHERE name LIKE '%輸入內容%';

-- 攻擊者輸入:
' UNION SELECT username, password FROM users --

-- 產生的 SQL:
SELECT name, price FROM products
WHERE name LIKE '%' UNION SELECT username, password FROM users --%';

這樣攻擊者就能在商品清單的頁面上看到所有使用者的帳號和密碼。這就是 UNION-based SQL Injection 的典型攻擊方式。

刪除資料表

更危險的情況是,攻擊者可以過注入破壞性的 SQL 指令:

-- 攻擊者輸入:
'; DROP TABLE users; --

-- 產生的 SQL:
SELECT * FROM users
WHERE username = ''; DROP TABLE users; --'
AND password = '';

如果資料庫支援多行指令執行,整個 users 資料表就會被刪除,造成無法挖回的損失。

SQL Injection 防範方法(How to Prevent SQL Injection)

防範 SQL Injection 最重要的原則是:永遠不要將使用者輸入的內容直接拼接到 SQL 查詢字串中。以下是幾種主要的防範方法:

參數化查詢(Parameterized Queries)

參數化查詢(也稱為 Prepared Statements)是防範 SQL Injection 最有效且最推薦的方法。它將 SQL 查詢的結構和資料分開處理,資料庫會將參數視為純資料,而不是 SQL 指令的一部分。

以下是不同程式語言使用參數化查詢的範例:

-- ✘ 不安全的寫法(字串拼接)
SELECT * FROM users
WHERE username = '' + input + '';

-- ✔ 安全的寫法(參數化查詢)
SELECT * FROM users
WHERE username = ?;

各種程式語言的實作方式:

# Python (MySQL Connector)
cursor.execute(
    "SELECT * FROM users WHERE username = %s AND password = %s",
    (input_username, input_password)
)
// PHP (PDO)
$stmt = $pdo->prepare(
    "SELECT * FROM users WHERE username = :username AND password = :password"
);
$stmt->execute([
    ':username' => $input_username,
    ':password' => $input_password
]);
// Java (JDBC)
PreparedStatement stmt = conn.prepareStatement(
    "SELECT * FROM users WHERE username = ? AND password = ?"
);
stmt.setString(1, inputUsername);
stmt.setString(2, inputPassword);
ResultSet rs = stmt.executeQuery();
// Node.js (mysql2)
const [rows] = await connection.execute(
    'SELECT * FROM users WHERE username = ? AND password = ?',
    [inputUsername, inputPassword]
);

當使用參數化查詢時,即使攻擊者輸入 ' OR '1'='1' --,資料庫也只會將它當作普通的字串資料來比對,而不會執行其中的 SQL 指令。

輸入驗證與過濾(Input Validation)

除了參數化查詢之外,對使用者輸入的內容進行驗證和過濾也是重要的防範層。常見的做法包括:

驗證方式說明範例
白名單驗證只允許特定格式的輸入帳號只允許英文字母和數字
長度限制限制輸入字串的最大長度帳號最多 20 個字元
特殊字元轉義將单引號等特殊字元進行轉義將 ‘ 轉為 ”
型別檢查確保輸入符合預期的資料型別ID 參數必須是整數
# Python 輸入驗證範例
import re

def validate_username(username):
    # 只允許英文字母、數字和底線,長度 3-20
    if re.match(r'^[a-zA-Z0-9_]{3,20}$', username):
        return True
    return False

def validate_id(user_id):
    # 確保是正整數
    try:
        return int(user_id) > 0
    except ValueError:
        return False

注意:輸入驗證應該作為額外的防護層,不能取代參數化查詢。因為単純的過濾可能被繞過,而參數化查詢才是根本的解決方案。

最小權限原則(Least Privilege)

資料庫帳號應該只給予應用程式必要的權限。例如,如果應用程式只需要讀取資料,就不要給予 DELETE 或 DROP 的權限:

-- 建立只有讀取權限的資料庫帳號
CREATE USER 'app_readonly'@'localhost'
IDENTIFIED BY 'strong_password';

-- 只給予 SELECT 權限
GRANT SELECT ON mydb.* TO 'app_readonly'@'localhost';

-- 如果需要寫入,只給必要的權限
GRANT SELECT, INSERT, UPDATE ON mydb.orders
TO 'app_write'@'localhost';

即使攻擊者成功注入 SQL,也無法執行超出權限範圍的操作,例如無法 DROP TABLE 或存取其他資料庫。

使用 ORM(Object-Relational Mapping)

現代的網頁框架通常提供 ORM 工具,它會自動使用參數化查詢來存取資料庫,大幅降低 SQL Injection 的風險:

# Python Django ORM
# ✔ 安全:ORM 自動處理參數化
user = User.objects.filter(
    username=input_username,
    password=input_password
).first()

# ✘ 不安全:直接使用 raw SQL 拼接
User.objects.raw(
    "SELECT * FROM users WHERE username = '" + input_username + "'"
)

錯誤訊息處理(Error Handling)

在正式環境中,不應該將資料庫的詳細錯誤訊息顯示給使用者。因為攻擊者可以利用這些資訊來推測資料庫的結構,進而發動更精準的攻擊。

# ✘ 不安全:顯示詳細錯誤訊息
try:
    cursor.execute(query)
except Exception as e:
    return str(e)  # 暴露資料庫結構資訊

# ✔ 安全:回傳通用錯誤訊息
try:
    cursor.execute(query)
except Exception as e:
    logging.error(f"Database error: {e}")  # 記錄到日誌
    return "系統發生錯誤,請稍後再試"  # 給使用者看的

防範方法整理表(Prevention Summary)

防範方法效果優先級
參數化查詢從根本上防止 SQL 注入,將資料與指令分離★★★ 必要
輸入驗證過濾不合法的輸入,提供額外保護★★★ 必要
最小權限限制攻擊成功後的危害範圍★★ 建議
使用 ORM自動處理參數化,減少人為疏失★★ 建議
錯誤訊息處理避免洩漏資料庫結構資訊★★ 建議
定期更新與檢測保持軟體更新,使用安全掃描工具★ 加分

延伸閱讀