June 3, 2026
PortSwigger 學習筆記|SQL Injection:從資料風險角度理解 Web Security
我一開始對 SQL Injection 的理解其實很基礎。過去我知道它大概是因為應用程式把 user input 直接放進 SQL query,導致有心人士可以透過特定字元或語法改變原本的查詢邏輯,進而讀取、繞過或影響資料庫中的資訊。但在真正開始做 PortSwigger…
Chun Han
8 min read
我一開始對 SQL Injection 的理解其實很基礎。過去我知道它大概是因為應用程式把 user input 直接放進 SQL query,導致有心人士可以透過特定字元或語法改變原本的查詢邏輯,進而讀取、繞過或影響資料庫中的資訊。但在真正開始做 PortSwigger Web Security Academy 的 labs 之前,我其實不太清楚這些攻擊是怎麼被操作的,也不清楚一個看似普通的 URL parameter、cookie,或輸入欄位,如何一步步影響後端資料庫。
我選擇從 PortSwigger 開始,是因為它不只是提供理論說明,而是可以搭配 Burp Suite 進行實際操作。對初學者來說,這讓 SQL Injection 不再只是抽象的資安名詞,而是可以透過 HTTP request、response、cookie、URL parameter 和 database query 之間的互動被具體理解。特別是我本身有 SQL 和資料分析背景,所以這個學習過程也讓我開始看到 SQL knowledge 和 cybersecurity 之間的連結。
這次最讓我意外的是,SQL Injection 不一定需要網站直接把資料顯示出來。即使頁面看起來沒有明顯變化,攻擊者仍然可能透過 response content 的差異、database error、response time,甚至 DNS interaction 等方式推斷資料。這讓我重新理解了一件事:資料沒有直接出現在頁面上,不代表資料就是安全的。
SQL Injection 的核心:User input 不應該變成 SQL logic
我現在對 SQL Injection 的理解是,它的本質不是單純輸入一些特殊字元,而是攻擊者利用 application 和 database 之間不安全的資料拼接,讓 user input 從「資料值」變成「SQL query logic」的一部分。
舉例來說,如果應用程式用 string concatenation 直接建立 SQL query:
String query = "SELECT * FROM products WHERE category = '" + input + "'";String query = "SELECT * FROM products WHERE category = '" + input + "'";正常情況下,開發者只是希望根據使用者選擇的 category 查詢產品。但如果 input 被直接拼進 SQL query,使用者輸入的內容就有機會改變原本的 SQL 結構。當攻擊者輸入包含單引號、註解符號或額外條件的字串時,資料庫可能不再只是把它視為一個普通的 category,而是把其中一部分解讀成 SQL 語法。
這也是為什麼 SQL Injection 的防禦重點不是單純「過濾特殊符號」而已。比較根本的方式是使用 parameterized queries 或 prepared statements,讓 SQL query 的結構和 user input 被分開處理。換句話說,使用者輸入應該只被當成資料,而不應該有能力改變原本 query 的邏輯。
我在 Labs 裡看到的幾種 SQL Injection 型態
在 PortSwigger 的 labs 裡,我發現 SQL Injection 並不是只有一種固定形式。不同情境下,攻擊者能觀察到的訊號不一樣,因此使用的判斷方式也會不同。有些情況下,資料會直接顯示在頁面上;有些時候,資料不會被直接回傳,但仍然可以透過間接訊號推斷結果。
UNION-based SQL Injection:當查詢結果會直接回到頁面
UNION-based SQL Injection 是相對直覺的一類。當應用程式會把 SQL query 的結果顯示在 response 中,攻擊者就可能利用 UNION SELECT 把其他資料表的查詢結果合併到原本頁面裡。
在 lab 中,這類題目通常會先確認原本 query 有幾個 columns,以及哪些 columns 可以顯示文字。接著,再透過 database metadata 找出敏感資料表與欄位名稱,例如 users table、username column 和 password column。這讓我意識到,資料庫裡不只是業務資料本身有風險,metadata 也可能幫助攻擊者定位敏感資料的位置。
Error-based SQL Injection:當錯誤訊息本身成為資訊來源
Error-based SQL Injection 利用的是過度詳細的 database error message。當應用程式把 SQL error 直接顯示給使用者時,錯誤訊息可能暴露 query structure、table name、column name,甚至在某些情況下直接洩漏資料值。
例如在某個 lab 裡,攻擊者可以故意讓資料庫進行不合理的型別轉換,例如把文字型的 password 轉成 integer。因為 password 通常不是純數字,轉換會失敗,而詳細錯誤訊息可能會把原本的 password 顯示出來。這讓我理解到,錯誤訊息不只是 debugging 資訊,在正式環境中也可能變成資料外洩的管道。
Blind SQL Injection:當資料不顯示,但仍能被推斷
Blind SQL Injection 是這次學習中最讓我意外的部分。所謂 blind,並不是代表攻擊者完全沒有線索,而是指 SQL query 的結果不會直接顯示在頁面上。即使如此,攻擊者仍然可以透過其他訊號判斷 SQL 條件是否成立。
最基本的例子是 conditional response。當某個 SQL 條件成立時,頁面可能會出現特定文字;當條件不成立時,該文字則消失。在 lab 裡,這個訊號可能是 Welcome back 是否出現在 response 中。攻擊者可以把問題拆成一連串 true / false 判斷,逐步推斷敏感資料。
另一種是 conditional error。在這類情境中,攻擊者會設計 payload,讓條件成立時故意觸發 database error;條件不成立時則正常執行。因此,500 Internal Server Error 或 200 OK 就成為判斷條件真假的訊號。這裡需要注意的是,並不是所有情況下 500 都代表「猜對了」,而是因為 payload 被設計成讓 true condition 觸發錯誤。
如果頁面內容和錯誤狀態都沒有差異,還可以透過 time-based blind SQL injection 判斷。這類方法會在條件成立時讓資料庫延遲幾秒回應;條件不成立時則正常回應。雖然這種方式比直接看到資料慢很多,但它說明了另一個重要觀念:資料不一定要出現在 response 裡,仍然可能透過 side channel 被推斷。
Out-of-band SQL Injection:當結果從另一條通道回來
Out-of-band SQL Injection 則是更間接的一種方式。當 HTTP response 完全不受 SQL query 影響時,攻擊者可以嘗試讓後端系統對外部 domain 發出 DNS lookup 或其他 network interaction。如果外部系統收到互動,就代表 payload 被執行。
這類技巧讓我第一次理解到,攻擊結果不一定要從原本的 HTTP response 回來。它可能透過 DNS 這類外部通道傳出。換句話說,即使前端頁面看起來完全正常,後端仍可能在使用者看不到的地方發生外部互動。
Second-order SQL Injection:資料先被存起來,之後才觸發
Second-order SQL Injection 比較像真實業務流程中的隱性風險。它不是在 user input 被提交的當下立刻觸發,而是 payload 先被存進 database。之後,當系統在另一個功能中讀取這筆已儲存資料,並再次不安全地拼進 SQL query 時,漏洞才真正發生。
這種情境更容易被忽略,因為第一次輸入時系統可能沒有任何異常。問題可能出現在後續流程,例如報表、後台查詢、訂單查詢或其他內部功能。這也提醒我,已經存進 database 的資料不代表就是安全資料。只要資料之後還會被重新使用,就仍然需要在每一次查詢中用安全的方式處理。
如何防止 SQL Injection
做完這些 labs 後,我覺得 SQL Injection 的防禦重點不應該只放在「過濾特殊字元」。因為不同 database 有不同語法,攻擊方式也可能從最直覺的 UNION SELECT,延伸到 error-based、blind、time-based,甚至 out-of-band 的形式。只靠阻擋某幾個符號,很容易漏掉變形寫法。
比較根本的防禦方式,是使用 parameterized queries 或 prepared statements。它們的核心概念是:先固定 SQL query 的結構,再把 user input 作為參數傳入。這樣即使 input 裡面包含單引號、註解符號或看起來像 SQL 的內容,也只會被當成普通資料,不會改變原本的 query logic。
例如,不安全的寫法是直接把 input 拼進 SQL query:
String query = "SELECT * FROM products WHERE category = '" + input + "'";String query = "SELECT * FROM products WHERE category = '" + input + "'";這種寫法的問題在於,input 可能不再只是資料值,而是有機會變成 SQL 語法的一部分。相對來說,prepared statement 會把 query structure 和 input data 分開:
PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM products WHERE category = ?"
);
statement.setString(1, input);PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM products WHERE category = ?"
);
statement.setString(1, input);這裡的 ? 是 placeholder,setString(1, input) 則是把 user input 作為第一個參數傳入。資料庫會把這個 input 視為一個字串值,而不是可以被執行的 SQL 邏輯。
除了 prepared statements,系統也不應該在正式環境中顯示過度詳細的 database error message。因為錯誤訊息可能暴露 SQL query structure、table name、column name,甚至在某些情況下洩漏實際資料值。對使用者來說,前端應該只顯示一般性的錯誤訊息;真正詳細的錯誤內容應該記錄在 server logs,並限制只有內部人員可以查看。
另外,database account 也應該採用 least privilege。應用程式連接資料庫時,不應該使用權限過高的帳號。即使 SQL Injection 發生,較低的資料庫權限也可以降低攻擊者能讀取、修改或刪除資料的範圍。
最後,Second-order SQL Injection 也提醒我,已經存進 database 的資料不代表就是可信資料。當系統之後重新使用這些資料時,仍然應該使用安全的 query 寫法,而不是因為資料已經在內部 database 裡,就假設它不會造成風險。
對我來說,防止 SQL Injection 的核心原則可以簡化成一句話:user input 應該永遠被當成 data,而不是 query logic。只要 application 能清楚分開 SQL 結構和使用者輸入,就能大幅降低 SQL Injection 的風險。