Day 6 時,我讓輸入落在 HTML 文字位置,確認探針不只是出現在回應裡,還真的在 Chrome 中執行。Day 7~9 則把那次結果交給 AI 整理、匯出報告,最後留下作者核對與修訂紀錄。
但 Day 6 的探針有一個前提:輸入位於標籤之間,可以直接插入一個 <img> 元素。如果應用程式把同一個查詢參數放進現有元素的屬性值,瀏覽器會用另一套規則解析。我不能因為頁面包含 onerror 這幾個字,就沿用上一版的判斷。
所以今天回到 XSS 偵測器,選定一個具體位置:雙引號包住的 <img src="...">。我會在本機準備弱點頁、編碼頁與不反射頁,保存伺服器原始回應,再用瀏覽器核對屬性結構及執行結果。
這次測試頁的核心結構是:
<img id="result-image" src="使用者輸入" alt="搜尋結果圖片">
使用者輸入原本應該只是 src 的值。若把原始雙引號直接放進這個值,HTML 解析器會結束目前的屬性值;接著的空格與文字便有機會形成新的屬性。明確區分了原始 " 與 &:前者結束雙引號屬性值,後者進入字元參照解析。
我使用的探針從一個不存在的圖片路徑開始,後面接上引號、事件處理屬性,以及本次唯一識別碼:
/day10-missing.png" onerror="document.documentElement.setAttribute('data-day10-xss','本次識別碼');alert(1)" data-day10-probe="本次識別碼
頁面模板最後還有原本用來關閉 src 的雙引號。因此,若輸入完全未經編碼,第一個引號會提早結束 src;後面的 onerror 與 data-day10-probe 會成為圖片的獨立屬性。圖片請求由本機測試網站回傳 404,用來穩定觸發載入失敗的 error 事件。
探針的動作仍與 Day 6 一樣簡單:先把識別碼寫到 <html> 的 data-day10-xss,再執行 alert(1)。它不讀取 Cookie,也不對外傳送資料。
我讓網站只在 127.0.0.1 上接受測試,並讓 q 參數進入同一個 img#result-image 的 src 位置。3 個端點的差別只有輸入處理方式:
| 端點 | src 的來源 |
預期觀察 |
|---|---|---|
/attribute-vulnerable |
直接放入 q |
引號跳出 src,新增 onerror 並執行 |
/attribute-safe |
先以 html.escape(value, quote=True) 編碼 |
探針仍在 src 值內,沒有獨立的 onerror |
/attribute-not-reflected |
固定使用 /fixed.png |
本次探針不出現在回應 |
今天的重點是把「看到文字」和「真的新增屬性」分開。偵測器先保存原始 text/html 回應,再解析其中 img#result-image 的屬性;接著讓 Chrome 載入相同網址,讀回瀏覽器 DOM 裡的 src、onerror 與 data-day10-probe。
執行證據也沿用 Day 6 的做法:監聽並關閉訊息為 1 的 alert,再確認 <html> 的 data-day10-xss 等於本次探針識別碼。只有這兩項同時出現,才標記 confirmed_xss。如果獨立的 onerror 已形成,卻沒有完整執行證據,分類會是 html_injection_only;有反射而沒有新增屬性與執行,則是 reflected_but_not_executed。
我也保留原有查詢參數,限制偵測目標為本機位址,並拒絕會破壞探針語法的識別碼。這些限制讓今天的測試範圍固定,也方便重現結果。
我在專案根目錄執行:
python3 day-10/run_experiment.py
這支程式會暫時啟動本機網站,依序測試 3 個端點,並把完整 JSON、原始 HTML 回應和執行摘要留在 day-10/results/。這次使用 Python 3.14.4 與 Google Chrome 153.0.8010.36;3 個頁面都回傳 200 與 text/html。
弱點頁原始回應中的圖片標籤如下:
<img id="result-image" src="/day10-missing.png" onerror="document.documentElement.setAttribute('data-day10-xss','day10-8c697d71d7558548');alert(1)" data-day10-probe="day10-8c697d71d7558548" alt="搜尋結果圖片">
解析原始回應時,src 只剩 /day10-missing.png;onerror 與 data-day10-probe 已是獨立屬性。Chrome 中的 DOM 也讀到相同結構。圖片載入失敗後,偵測器捕捉到訊息為 1 的 alert,並在 <html> 讀回與探針相同的 data-day10-xss。因此這次分類為 confirmed_xss。
這不是單靠原始回應中出現 onerror 一詞得出的結論。原始屬性結構、DOM 屬性結構、對話框事件與執行後識別碼都與本次探針相符。
編碼頁最容易誤判。它的原始回應仍然包含 onerror 文字,但關鍵位置已經變成 ":
<img id="result-image" src="/day10-missing.png" onerror="document.documentElement.setAttribute('data-day10-xss','day10-e33c70e013728f7d');alert(1)" data-day10-probe="day10-e33c70e013728f7d" alt="搜尋結果圖片">
HTML 解析器讀到 " 時,會把它解成引號字元,放進目前的 src 屬性值,而不是拿這個解碼後的引號重新結束屬性。
所以,從 Chrome 讀回 src,仍會看到類似 " onerror=" 的字串;但 getAttribute('onerror') 與 getAttribute('data-day10-probe') 都是 null。原始回應解析與 Chrome DOM 均未出現獨立事件屬性,也沒有 alert 或執行旗標。這一頁的分類是 reflected_but_not_executed。
這個對照提醒我:單純搜尋回應或 DOM 字串中的 onerror 會造成誤報。必須確認它究竟是屬性名稱,還是某個屬性值裡的普通文字。
| 端點 | 回應形成獨立 onerror |
DOM 有獨立 onerror |
alert(1) 與識別碼 |
分類 |
|---|---|---|---|---|
| 弱點頁 | 是 | 是 | 都觀察到 | confirmed_xss |
| 編碼頁 | 否 | 否 | 都未觀察到 | reflected_but_not_executed |
| 不反射頁 | 否 | 否 | 都未觀察到 | not_reflected |
不反射頁的 src 固定為 /fixed.png,原始回應裡沒有本次識別碼,因此不是「反射但安全」;它是另一種情況。3 組實測的原始回應雜湊與判斷摘要也保存在 results/run-summary.json,可對照各自的 JSON 與原始回應文字檔。
另外,我執行 Day 10 的 6 個自動測試,涵蓋三種端點、既有查詢參數、本機目標限制與探針識別碼限制;6 個都通過。Day 6 的 5 個瀏覽器整合測試也重新執行並通過。測試會實際啟動 Chrome,確認對話框和 DOM 狀態,而不是只比對字串。
confirmed_xss 確認的是:在這個本機頁面上,q 原樣進入雙引號包住的 img src,本次探針新增了獨立的 onerror,而 Chrome 確實執行它。它沒有表示其他參數、其他頁面或其他 HTML 屬性都已檢查。
這次也沒有測試 URL 協定、href、style、JavaScript 字串、DOM 型 XSS、POST、儲存型 XSS,或需要使用者互動才會觸發的事件。src 即使沒有被引號跳脫,仍有 URL 的語意;實際產品若接受使用者指定的網址,還需要依用途做 URL 驗證。OWASP 也把 URL 情境列為需要另外處理的輸出位置。
測試頁未設定 CSP。若頁面設有嚴格的 CSP,瀏覽器可能阻擋行內事件處理器;「沒有跳出 alert」便不能單獨證明 HTML 結構沒有被改變。CSP 是額外防線,仍應把不可信資料放入正確位置並依情境處理。
今天沒有重新呼叫 Gemini,也沒有把 Day 10 的結果交給 Day 7~9 的報告流程。這次先確認底層偵測器真的能區分屬性跳脫、編碼後反射及不反射,下一步再考慮如何讓 AI 報告保留這些差異。
今天的關鍵不是換一段更長的 Payload,而是換了輸出位置後,重新確認瀏覽器如何解析。弱點頁裡,原始引號讓輸入跳出 src,新增 onerror 並執行;編碼頁裡,解碼後的引號仍留在 src 值中,因此沒有形成事件屬性。
下一次遇到反射輸入,我會先問它落在 HTML 的哪個位置,再決定如何設計探針與判斷證據。只看關鍵字,回答不了瀏覽器究竟做了什麼。
那就…
明天見!