前幾天,我追蹤的是輸入隨當次請求進入回應,或從網址片段進入瀏覽器 DOM。今天換成留言功能:表單先送出文字,伺服器保存,再由後續的讀取請求產生頁面。這段時間差是儲存型 XSS 的重點。MDN 用留言作為例子說明:網站接受並保存內容,之後又把內容放進提供給讀者的頁面,若沒有妥善處理,就可能形成儲存型 XSS
我要分開回答 4 件事:表單送到哪裡、POST 是否真的保存、之後 GET 回傳了什麼、Chrome 是否執行本次探針。只看到 201 或回應裡有 alert(1) 文字,都還不能回答最後一題。
我在 127.0.0.1 建立 3 組本機表單。每個表單都使用 method="post",欄位名稱是 message,但提交與顯示方式不同:
| 組別 | 提交後的處理 | 讀取頁的處理 |
|---|---|---|
| 原樣插入 | 保存留言 | 直接放入 HTML 的 <div> |
| HTML 跳脫 | 保存留言 | 將留言中的特殊字元轉為 HTML 實體 |
| 拒絕提交 | 回傳 422,不保存 | 顯示固定的「沒有已儲存內容」 |
本次只把資料存在伺服器記憶體中,並在每組開始前清空。這足以檢查「POST 之後,另一個 GET 再讀到」的流程;它沒有模擬資料庫、不同帳號或長期保存。
我保留每組表單的完整原始 HTML,也另外解析 action、method 和欄位名稱。如此一來,後續整理表單時,可以從原始檔重新核對,而不只相信程式寫出的摘要。
我為每組產生獨立識別碼,將下面形狀的探針作為留言送出:
<img src="/day18-missing.png" alt="" onerror="document.documentElement.setAttribute('data-day18-xss','本次識別碼');alert(1)">
圖片只指向本機刻意不存在的檔案。若瀏覽器真的把留言解析成 <img>,載入失敗會觸發 onerror。我要求 Chrome 同時捕捉到訊息為 1 的對話框,並在 <html> 讀回與本次識別碼相符的 data-day18-xss,才把固定探針記為已執行。探針沒有讀取 Cookie,也沒有把資料送往外部。
MDN 的 XSS 範例同樣說明,圖片載入錯誤事件可以讓插入的 HTML 執行程式;所以我需要檢查 DOM 真的形成元素,而不能只在回應文字裡搜尋 <img。
我從專案根目錄執行:
.venv/bin/python day-18/run_experiment.py
程式先取得每組的表單原始 HTML,再以 message 提交固定探針。POST 的原始請求與回應分開保存;完成提交後,程式用另一個 GET 取得讀取頁的原始 HTML,最後才讓 Chrome 開啟該讀取頁。三組的表單都是 POST /{組別}/submit,後續讀取都是 GET /{組別}/view。伺服器的請求紀錄也保留了這個先後順序。
弱點組與跳脫組的 POST 都回傳 201,而且伺服器記憶體都保存了完整探針;拒絕組回傳 422,伺服器沒有保存它。可見「保存成功」只回答了資料是否留在伺服器,不等於執行。
弱點組的後續 GET 把探針原樣放進 HTML。Chrome 在 #result 中建立了真正的 <img>,並請求不存在的 /day18-missing.png。本次收到 alert(1),DOM 旗標是 day18-2070ed412ac1522e,與送出的識別碼相同。依雙重條件,這個本機讀取頁上的固定探針已執行,分類為 confirmed_stored_xss。
跳脫組也保存了完整留言,但後續 GET 將開頭的 <img 寫成 <img。Chrome 的 textContent 仍能讀回完整探針字面文字,DOM 裡卻沒有圖片元素,也沒有對話框或相符旗標。本次分類為 stored_as_text。
拒絕組的 POST 直接回傳 422,後續讀取頁只顯示「沒有已儲存內容」。這次探針沒有被保存,也沒有在讀取頁出現,分類為 submission_rejected。這是本組特意設計的對照;一般留言功能不能靠一律拒絕輸入來代替合適的顯示處理。
| 組別 | POST | 後續 GET | Chrome 雙重訊號 | 本次分類 |
|---|---|---|---|---|
| 原樣插入 | 201,已保存 | 原始 <img> |
alert(1) 與相符旗標 |
confirmed_stored_xss |
| HTML 跳脫 | 201,已保存 | <img,只作文字 |
都沒有 | stored_as_text |
| 拒絕提交 | 422,未保存 | 固定文字 | 都沒有 | submission_rejected |
我還執行了 4 個測試,檢查提交與讀取確實分開、原始檔的 SHA-256 一致、兩種未執行情境沒有被誤判,以及偵測器拒絕遠端目標。完整表單、POST、GET、Chrome 結果都保存在 day-18/results/。
這次弱點組的證據不是單一的「回應包含探針」。同一段輸入先進入表單 POST、留在伺服器記憶體,再由後續 GET 進入 HTML,最後在 Chrome 形成圖片元素並出現雙重執行訊號。跳脫組停在「保存後作文字顯示」;拒絕組則在提交階段就停止。
這些結論只適用於本機、單一 message 欄位及固定 img onerror 探針。我沒有測試持久資料庫、跨帳號瀏覽、其他輸出位置、CSP 或正式網站;也不能因為跳脫組與拒絕組沒有觸發,就推論整個網站安全。
那就…
明天見!