什麼是 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 教學):
| id | username | password | role |
|---|---|---|---|
| 1 | admin | a1b2c3d4 | admin |
| 2 | john | pass123 | user |
| 3 | mary | mary5678 | user |
攻擊者在登入表單的帳號欄位輸入以下內容,密碼欄位隨便輸入:
-- 帳號欄位輸入:
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 | 自動處理參數化,減少人為疏失 | ★★ 建議 |
| 錯誤訊息處理 | 避免洩漏資料庫結構資訊 | ★★ 建議 |
| 定期更新與檢測 | 保持軟體更新,使用安全掃描工具 | ★ 加分 |
延伸閱讀
- SQL 教學 — 完整 SQL 語法教學
- SQL SELECT — 資料查詢語法
- SQL WHERE — 條件查詢教學
- SQL Subquery — 子查詢教學
- SQL UNION — 合併查詢教學
- SQL VIEW — 檢視表教學
- CREATE TABLE — 建立資料表
- SQL Constraints — 資料約束教學