昨天整理 XSS 的形成過程時,我把判斷拆成幾個階段:找到輸入點、追蹤輸入、分析輸出位置,最後才是驗證瀏覽器行為。
如果工具只看到輸入出現在回應中,最多只能證明「發生反射」;即使輸入成為新的 HTML 元素,也還沒有證明 JavaScript 能夠執行。
所以今天,來測試看看自己能不能跟AI一起做出一個基本的工具
我希望工具能區分 3 種情況:
| 情況 | 工具要看到的證據 |
|---|---|
| JavaScript 實際執行 | 執行後 DOM 出現本次探針寫入的唯一識別碼 |
| 輸入被反射,但沒有執行 | 回應中有識別碼,執行後 DOM 沒有旗標 |
| 輸入沒有被反射 | 回應與執行後 DOM 都找不到本次識別碼 |
第一種情況同時包含輸入、瀏覽器執行與可核對的結果,才足以在這個固定測試案例中標為 confirmed_xss。
alert(1),但讓工具自動處理常見的 XSS 示範會使用 alert(1)。既然今天操作的是自己建立的本機環境,我就直接使用它,讓執行結果保持直觀。
為了確認這個對話框確實來自本次探針,我也保留唯一識別碼。圖片載入失敗時,事件處理器會先在 <html> 寫入識別碼,再呼叫 alert(1):
<img
src="/day6-missing.png"
onerror="document.documentElement.setAttribute(
'data-day6-xss',
'owo'
); alert(1)"
>
如果這段程式成功執行,瀏覽器中的 <html> 會變成:
<html lang="zh-Hant" data-day6-xss="owo">
探針不會讀取 Cookie、不會傳送資料,也不會修改網站狀態。除了顯示 alert(1),它只留下本次測試的執行旗標,方便程式核對。
為了讓結果可以重現,我建立了一個只在 127.0.0.1 上執行的測試網站,並準備 3 個端點。
弱點示範頁直接把 q 放進 HTML:
body = PAGE.format(title="弱點示範頁", value=value)
安全示範頁先做 HTML 編碼:
body = PAGE.format(title="安全示範頁", value=html.escape(value))
第三個頁面則完全不顯示 q,用來確認工具不會在沒有反射時回報相同結果。
這些頁面都是刻意建立的本地環境。偵測器也只接受 localhost、127.0.0.1 與 ::1,不會把初版實驗送到外部網站。
執行流程可以整理成:
產生owo
↓
把測試探針放進 q 參數
↓
保存伺服器的原始 HTML 回應
↓
以無頭模式的 Chrome 載入相同網址
↓
捕捉並關閉內容為 1 的 alert
↓
核對 data-day6-xss 是否也等於本次識別碼
工具先保存原始回應,再啟動獨立的暫存瀏覽器設定檔。接著透過 Chrome DevTools Protocol 監聽 Page.javascriptDialogOpening 事件。
捕捉到對話框後,工具會保存類型與訊息,然後送出關閉對話框的指令:
if message.get("method") == "Page.javascriptDialogOpening":
params = message.get("params", {})
dialog_type = params.get("type")
dialog_message = params.get("message")
send("Page.handleJavaScriptDialog", {"accept": True})
頁面載入完成後,工具再讀取 data-day6-xss。只有對話框類型是 alert、訊息是 1,而且 DOM 識別碼也與本次探針相同時,才輸出 confirmed_xss。如果輸入能建立元素,卻沒有完整執行證據,就輸出 html_injection_only;有反射但沒有建立元素與執行時,則輸出 reflected_but_not_executed。
我先啟動本機測試網站:
python3 day-06/lab_server.py
接著分別測試 3 個端點:
python3 day-06/xss_detector.py http://127.0.0.1:8000/vulnerable
python3 day-06/xss_detector.py http://127.0.0.1:8000/safe
python3 day-06/xss_detector.py http://127.0.0.1:8000/not-reflected
實際結果如下:
| 端點 | 工具分類 | alert_observed |
execution_observed |
|---|---|---|---|
/vulnerable |
confirmed_xss |
true,訊息為 1 |
true |
/safe |
reflected_but_not_executed |
false |
false |
/not-reflected |
not_reflected |
false |
false |
3 次請求都收到 200 與 text/html 回應,分類符合預期。
弱點頁先觸發內容為 1 的 alert。工具將它關閉後,也從瀏覽器讀到:
<html lang="zh-Hant" data-day6-xss="owo">
這個屬性不存在於伺服器原始頁面,只能由探針中的 JavaScript 寫入。alert(1) 事件與唯一 DOM 識別碼同時成立,讓工具不必只依賴其中一種訊號。
安全頁雖然也在回應中保留識別碼,但標記已經變成文字:
<img src="/day6-missing.png" ... >
Chrome 不會把它建立成圖片元素,onerror 也不會執行。這個對照說明「輸入出現在回應中」與「程式真的執行」是兩件不同的事。
除了分別執行 3 次,我也加入 5 個自動測試,檢查弱點頁、安全頁、不反射頁面、原有查詢參數,以及外部網址限制。
python3 -m unittest discover -s day-06 -p 'test_*.py' -v
測試會真的啟動 Chrome,而不是模擬瀏覽器結果。這次 5 個測試全部通過,其中弱點頁的測試同時核對:
confirmed_xss。alert_observed 是 true。dialog_message 是 1。execution_observed 是 true。dom_marker 是正確的本次識別碼。是否執行成功,可以由瀏覽器對話框事件與 DOM 旗標共同判斷,不需要請模型猜測。
後續接回 AI Web Security Agent 時,我希望 AI 負責整合原始回應、HTML 結構、alert 事件、DOM 識別碼與限制,並轉成容易閱讀的報告。底層工具仍要保留這些原始證據,讓每個結論都能回頭核對。
這樣的分工延續前幾天的原則:先取得可重現的證據,再讓 AI 協助分析。
今天,我完成了第一個能實際驗證 XSS 執行的工具。它會送出包含 alert(1) 的唯一探針、保存原始回應、啟動 Chrome、捕捉並關閉對話框,最後再讀回 DOM 旗標。
在弱點頁中,JavaScript 確實觸發 alert(1),因此工具能把結果標為 confirmed_xss;安全頁與不反射頁則沒有出現對話框或執行旗標。
接下來,我會逐步加入不同的輸出情境,避免工具只能辨認這一種固定的 HTML 文字位置。
那就…
明天見!