Learning Objectives
- 說明 SQL 注入怎麼發生,並用參數化查詢修補
- 說明 XSS 怎麼發生、Jinja2 的自動跳脫做了什麼、safe 過濾器的風險,以及 HttpOnly 的作用
- 說明 CSRF 怎麼發生,並用 CSRF token 與 SameSite 防護
- 說出金鑰管理、除錯模式、登入限流、檔案上傳各要注意什麼
實際攻擊三個模擬網站,再修好它們
這一課會實際攻擊三個網站,再把漏洞修掉。這三個網站都是模擬的,只在你自己的瀏覽器裡執行,不會連到任何真實的伺服器。
這些技巧只能用在自己的程式,或已取得書面授權的測試。 對別人的網站嘗試,即使只是好奇、沒有造成損害,也可能違法。發現真實網站有漏洞時,不要繼續測試,依對方公布的管道回報。
OWASP Top 10:2025 整理了最常見的網站安全風險,這一課和上一課碰到的都在裡面:
| 這一課的主題 | 對應的類別 |
|---|---|
| 上一課的權限漏洞、這一課的 CSRF | A01 存取控制失效(Broken Access Control) |
| SQL 注入、XSS | A05 注入(Injection) |
| 除錯模式、預設設定沒改 | A02 安全設定錯誤(Security Misconfiguration) |
三種攻擊有共同的原因:把不可信的輸入,混進了會被執行的東西(SQL、HTML),或者瀏覽器自動帶上的 Cookie 被當成使用者的意願。
逐步看:一段輸入改變了 SQL 的意思
同一組輸入送給兩個版本。先注意 v1 組出來的 SQL,尤其是 -- 之後的內容;再看 v2 的 SQL 為什麼不受影響。
SQL 注入的防線是參數化查詢
OWASP 建議的主要防線是參數化查詢:SQL 的結構和資料分開送,資料再怎麼寫都不會變成 SQL。
| 不算防線 | 為什麼 |
|---|---|
| 把單引號過濾掉 | 攻擊手法很多,也會誤傷真的有單引號的資料(O'Brien) |
| 前端驗證 email 格式 | 請求不一定來自你的頁面(上一課) |
f-string 或 % 組 SQL |
和 + 接字串是同一件事 |
欄位名稱與排序方向不能當參數:ORDER BY ? 不會照你想的運作。這類地方要走白名單:只接受程式裡列出來的幾個值,其他一律拒絕(第 12 課)。
逐步看:自動跳脫做了什麼
模板把同一段字串輸出兩次。按到最後一步,比較輸出區的兩行。
XSS 的防線
剛才兩個修改的作用不同:
- 拿掉
|safe(根本解法):Flask 預設讓 Jinja2 自動跳脫,只有你明確標示|safe或Markup時才不跳脫。不要對使用者輸入用這兩個。 要讓使用者寫格式,就先經過白名單過濾的函式庫。 HttpOnly(降低損失):腳本讀不到 Cookie,但腳本還在執行,一樣能以使用者的身分送請求。Flask 的 Session Cookie 預設就有 HttpOnly,你自己set_cookie的要自己加。
還要注意:
- 屬性值一定要加引號:
<input value="{{ value }}">。沒有引號時,內容可以插入新的屬性。 - 自動跳脫擋不住
<a href="{{ url }}">裡的javascript:網址,使用者提供的網址要檢查開頭是http://或https://。 - 在前端 JavaScript 用
textContent,不要用innerHTML接資料(第 13 課)。 - CSP(Content-Security-Policy 回應標頭)告訴瀏覽器只能載入哪些來源的腳本,即使有人注入成功也較難執行,是額外的一層防護。
CSRF:別的網站讓你的瀏覽器送出請求
CSRF(Cross-Site Request Forgery,跨站請求偽造)不需要偷到任何東西。流程是:
- 你登入了 A 網站,瀏覽器保存著 A 的 Session Cookie
- 你開了 B 網站,B 的頁面有一個送往 A 的表單
- 你按下按鈕(或頁面自己送出),瀏覽器自動帶上 A 的 Cookie
- A 看到有效的 Cookie,以為是你本人的操作
攻擊者看不到回應,也讀不到 Cookie。但改信箱、轉帳、刪資料這類動作,只要請求送到就完成了。
防線的想法:要求請求裡多帶一個 B 網站拿不到的值,這就是 CSRF token。它在你自己的頁面裡以隱藏欄位送出;B 網站因為同源政策(第 13 課)讀不到你網站的回應,所以無從得知。
逐步看:手寫一次 CSRF token
程式先取得 token,再用三種方式送出修改信箱的請求。
CSRF 的防線與其他該做的事
CSRF 的防線
- 會改變資料的動作一律用 POST(或 PUT、DELETE),不要用 GET。
- CSRF token:實際開發用 Flask-WTF 的
CSRFProtect,不必自己寫。 SameSite:Lax不會在跨站 POST 帶上 Cookie,Strict連從別的網站點連結進來也不帶。Flask 的 Session Cookie 預設沒有設定,要自己加。OWASP 的立場是 SameSite 屬於額外一層,不取代 token,所以兩個都做。
其他該做的事
| 項目 | 做法 |
|---|---|
secret_key、API 金鑰 |
不寫死在程式裡,不進 git,從環境變數讀(第 19 課)。金鑰一旦進過 git,即使之後刪掉,歷史紀錄裡還有,要換一把新的 |
| 除錯模式 | 正式環境一定要關。Flask 文件說明除錯器可以從瀏覽器執行任意 Python 程式 |
| 登入嘗試 | 限制次數,否則可以一直猜密碼。可以用 Flask-Limiter |
| 上傳檔案 | 不能只看副檔名,要檢查內容、限制大小;檔名用 secure_filename 處理;不要存在 static 裡直接給人開 |
| 查詢與輸出 | 這一課的參數化與自動跳脫 |
更完整的清單請看 OWASP Top 10:2025。
動手做:修好搜尋功能的 SQL 注入
下面的搜尋用字串組 SQL,第二、三個測試會把不該出現的內容查出來。改成參數化查詢,讓惡意輸入只被當成一般文字,所以什麼都查不到。
提示:LIKE 的 % 不要寫在 SQL 裡,放在參數的值裡。程式最下面的測試不要改。
想一想
下面哪一行可以防止 SQL 注入?
下面的程式會印出什麼?
from flask import Flask, render_template_string
app = Flask(__name__)
with app.app_context():
print(render_template_string('{{ x }}', x='<b>Hi</b>'))
Session Cookie 設了 HttpOnly,網站上還有 XSS 漏洞。下面哪個說法正確?
為什麼 CSRF token 擋得住另一個網站送來的表單?
secret_key 不小心進了公開的 git repository。之後用一次新的 commit 把它刪掉,夠嗎?
重點整理
| 攻擊 | 原因 | 主要防線 |
|---|---|---|
| SQL 注入 | 輸入被當成 SQL 執行 | 參數化查詢;欄位名稱走白名單 |
| XSS | 輸入被當成 HTML、JavaScript 執行 | 自動跳脫,不對使用者輸入用 safe 過濾器;HttpOnly 與 CSP 降低損失 |
| CSRF | 瀏覽器自動帶 Cookie | CSRF token 加上 SameSite;改資料不用 GET |
- 金鑰不寫死、不進 git,外洩就換;正式環境關除錯模式;登入要限流;上傳檔案要檢查內容
- 這些技巧只用在自己的程式或獲得授權的測試
網站愈來愈大,也需要有方法確認修好的漏洞不會再回來。下一課把程式拆成多個檔案,並用自動測試把這些行為固定下來。