屬於 OWASP Top 10 的 A03 注入;與 XSS 同類但目標是資料庫。資料庫基礎見 交易與 ACID、索引。
目錄
- 什麼是 SQL Injection?
- 為什麼危險
- 核心成因:字串拼接
- 攻擊類型與範例
- 防護一:參數化查詢(主防線)
- 防護二:ORM 與查詢建構器
- 防護三:無法參數化的部分用白名單
- 防護四:最小權限與縱深防禦
- 常見誤區
- 常見問題
- 總結
什麼是 SQL Injection?
SQL Injection(SQL 注入) 是把惡意 SQL 片段透過使用者輸入「夾帶」進查詢,讓資料庫執行攻擊者預期外的指令。屬於 A03 注入——核心問題與 XSS 相同:把不可信輸入當成程式碼(SQL)執行。
輸入框 / URL 參數 / 標頭…的內容
→ 被字串拼接進 SQL
→ 改變了 SQL 的語意 → 資料庫照樣執行
為什麼危險
資料庫通常是系統最敏感的核心,SQL 注入可能導致:
- 讀取任意資料:拖走整個使用者表、密碼雜湊、個資
- 繞過認證:不需密碼就登入(見下方經典範例)
- 竄改 / 刪除資料:
UPDATE、DELETE、DROP TABLE - 進一步入侵:某些情況可執行系統指令(RCE)、讀檔
SQL 注入是歷史最悠久也最具破壞力的 Web 漏洞之一,一個拼接點就可能洩漏整個資料庫。
核心成因:字串拼接
幾乎所有 SQL 注入都源自把輸入字串直接拼進 SQL:
// ❌ 危險:把使用者輸入直接拼進查詢
const q = "SELECT * FROM users WHERE name = '" + name + "'";
當 name 是正常值沒事;但若 name 含 SQL 語法,整句的「結構」就被改寫了——資料庫無法分辨「哪些是指令、哪些是資料」。
攻擊類型與範例
1. 經典:繞過登入
-- 程式:SELECT * FROM users WHERE name='<輸入>' AND password='<輸入>'
-- 輸入 name: admin'--
SELECT * FROM users WHERE name='admin'--' AND password='...'
-- ↑ -- 把後面註解掉,password 檢查消失
-- 輸入: ' OR '1'='1
SELECT * FROM users WHERE name='' OR '1'='1' AND password='...'
-- '1'='1' 恆真 → 回傳所有列 → 繞過
2. UNION-based:撈出其他資料
用 UNION 把別的表的資料接到結果裡:
-- 輸入: ' UNION SELECT username, password FROM users--
SELECT name, email FROM products WHERE name='' UNION SELECT username, password FROM users--'
3. Blind(盲注)
回應看不到資料,但能用布林或時間差異一位元一位元推出資料:
-- 布林盲注:頁面有無變化推斷條件真假
... AND substring(password,1,1)='a'
-- 時間盲注:用延遲判斷真假
... AND IF(condition, SLEEP(5), 0)
防護一:參數化查詢(主防線)
最核心、最有效的防護:用參數化查詢 / prepared statement,讓資料庫把輸入永遠當「資料」,不當「指令」。
// ✅ 參數化:? / $1 是佔位符,輸入只會被當值
db.query("SELECT * FROM users WHERE name = ?", [name]); // MySQL
db.query("SELECT * FROM users WHERE name = $1", [name]); // PostgreSQL
# Python
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
// Java PreparedStatement
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
ps.setString(1, name);
原理:SQL 的結構(含佔位符)先送給資料庫編譯,輸入之後才綁定為純值——輸入再怎麼寫都無法改變查詢結構。
鐵則:永遠用參數化,永遠不要字串拼接使用者輸入進 SQL。 這一條就能擋掉絕大多數 SQL 注入。
防護二:ORM 與查詢建構器
ORM(如 Prisma、TypeORM、Hibernate、SQLAlchemy)與查詢建構器預設使用參數化,天生較安全:
// ORM:參數自動參數化
await prisma.user.findMany({ where: { name } }); // ✅ 安全
但要注意 raw query 出口:
// ⚠️ ORM 的原生查詢若字串拼接,照樣會注入
await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE name = '${name}'`); // ❌
// ✅ 用 tagged template / 參數版本
await prisma.$queryRaw`SELECT * FROM users WHERE name = ${name}`;
ORM 降低風險,但用了 ORM ≠ 免疫——只要走到字串拼接的 raw SQL 就破功。
防護三:無法參數化的部分用白名單
佔位符只能綁「值」,不能綁「識別字」(表名、欄位名、ORDER BY 方向、LIMIT 等結構部分)。這些若來自使用者,不能參數化,要用白名單:
// ❌ 危險:排序欄位來自使用者,直接拼
const q = `SELECT * FROM users ORDER BY ${sortColumn} ${dir}`;
// ✅ 白名單:只允許已知合法值
const allowedCols = { name: "name", date: "created_at" };
const col = allowedCols[sortColumn] ?? "created_at";
const direction = dir === "desc" ? "DESC" : "ASC";
const q = `SELECT * FROM users ORDER BY ${col} ${direction}`;
動態決定「表/欄位/排序」時最容易出包——記得:值用參數化,結構用白名單對映。
防護四:最小權限與縱深防禦
即使前面都做了,仍以多層防禦降低萬一被突破的衝擊:
- 資料庫最小權限:應用程式帳號只給必要權限(不要用 superuser;查詢用的帳號別有
DROP/FILE權限) - 輸入驗證:型別/格式驗證(如 id 必為數字)當額外一層,但不可取代參數化
- 錯誤訊息不外洩:別把 SQL 錯誤、堆疊回給前端(會幫攻擊者探查結構)
- WAF:可擋部分自動化注入,但只是輔助
常見誤區
1. 「我有跳脫特殊字元就安全了」
手動跳脫(escape)極易漏、且因資料庫/編碼而異,是最後手段。正解是參數化,讓你根本不必跳脫。
2. 「用了 ORM 就不會 SQL 注入」
多數情況安全,但 raw query / 字串拼接 出口照樣中招(如 $queryRawUnsafe)。
3. 「只有登入框要防」
任何進入 SQL 的輸入都要防:搜尋、排序參數、URL、API body、甚至 HTTP 標頭、Cookie。
4. 「前端擋了就好」
前端驗證可繞過,防護必須在後端(與其他注入/存取控制同理)。
常見問題
問題 1:參數化查詢為什麼能防注入?
因為查詢的結構先被資料庫編譯,輸入之後才以「純值」綁定——輸入無論含什麼 SQL 語法都不會改變查詢結構,只會被當成一個字串值比對。
問題 2:ORM 能完全防 SQL 注入嗎?
預設參數化的 ORM 能擋多數情況,但只要用到字串拼接的原生查詢(raw SQL)就可能破功。用 ORM 也要避免拼接、用其參數化 API。
問題 3:表名 / 排序欄位可以用參數化嗎?
不行。佔位符只能綁「值」,不能綁識別字(表名、欄位、ORDER BY、LIMIT 等結構)。這些若來自使用者要用白名單對映,不能拼接。
問題 4:SQL Injection 和 XSS 差在哪?
同屬注入,但目標不同:SQL 注入打資料庫(執行惡意 SQL);XSS 在受害者瀏覽器執行惡意 JS。共通根因都是「把輸入當程式碼」,防法各異(參數化 vs 輸出編碼)。
問題 5:跳脫(escape)和參數化哪個好?
參數化。跳脫易漏、隨資料庫/編碼而異、容易出錯;參數化從根本上把「指令」與「資料」分離,是業界標準做法。
總結
核心要點
- SQL Injection:把惡意 SQL 經輸入夾帶進查詢執行(A03 注入)→ 拖資料、繞認證、竄改/刪除
- 根因:字串拼接輸入進 SQL,資料庫分不清「指令」與「資料」
- 主防線:參數化查詢 / prepared statement——永遠別拼接使用者輸入
- ORM 預設參數化較安全,但 raw query 拼接照樣中招
- 結構部分(表/欄位/排序)不能參數化 → 用白名單對映
- 縱深:DB 最小權限、輸入驗證、不外洩 SQL 錯誤
快速參考
| 防護 | 作用 | 定位 |
|---|---|---|
| 參數化查詢 | 輸入永遠當值,不當指令 | 主防線 |
| ORM / 查詢建構器 | 預設參數化(避開 raw 拼接) | 主防線 |
| 白名單對映 | 表/欄位/排序等結構部分 | 必要時 |
| DB 最小權限 | 降低被突破的衝擊 | 縱深 |
| 輸入驗證 | 額外一層(不可取代參數化) | 縱深 |
建立日期:2026-06-18