Day 10,我把查詢參數放進雙引號包住的 HTML 屬性,分開觀察原始回應、DOM 屬性與執行結果。Day 11 再把這些差異交給 AI 整理。今天換一個輸出位置:頁面內的 JavaScript 字串。
同樣看見探針文字,不能直接沿用 HTML 屬性的結論。瀏覽器先解析 HTML 的 <script> 元素,接著才執行其中的 JavaScript;輸入只要跨過其中一層邊界,結果就可能不同。
這次我固定測試單引號字串,不把它擴大成所有 JavaScript 語法的掃描。我準備 3 個只在本機執行的頁面,讓 q 參數承載同型、各有唯一識別碼的探針,分別經過原樣插入、編碼插入與不反射處理,再用 Chrome 核對結果。
頁面的核心程式如下。query 的值最後會寫入 textContent,讓我能在 DOM 中讀回瀏覽器實際取得的字串:
<p id="query-output"></p>
<script id="query-script">
const query = '使用者輸入';
const output = document.getElementById('query-output');
output.textContent = query;
output.setAttribute('data-script-ran', 'true');
output.setAttribute('data-query-value', query);
</script>
弱點頁直接把 q 放在 '使用者輸入' 的位置。我的固定探針以單引號結束原本字串,寫入本次唯一識別碼,執行 alert(1),最後用 // 註解同一行剩下的引號:
';document.documentElement.setAttribute('data-day12-xss','本次識別碼');alert(1);//
這段探針只操作本機頁面的 DOM 並顯示對話框,不讀取 Cookie,也不傳送資料。偵測器只有同時捕捉到訊息為 1 的 alert,以及與本次識別碼相同的 DOM 旗標,才標記 confirmed_xss。若只出現其中一項,結果會保留為 execution_inconclusive。
我讓 3 個端點使用相同的畫面與後續 DOM 寫入程式,只改變 query 這一段如何產生:
| 端點 | query 的來源 |
預期觀察 |
|---|---|---|
/script-vulnerable |
原樣放入單引號字串 | 探針跳出字串並執行 |
/script-safe |
序列化為字串常值,並轉義 HTML 腳本邊界字元 | 完整探針留在字串資料中 |
/script-not-reflected |
固定使用「固定內容」 | 本次探針不進入回應 |
編碼頁使用 Python 的 json.dumps(value, ensure_ascii=True) 產生字串常值,接著將 <、>、& 分別寫成 \u003C、\u003E、\u0026。前一步處理 JavaScript 字串的引號與反斜線;後一步避免輸入中的 < 在 HTML 解析階段形成 </script>。Python 文件對 ensure_ascii 的保證是跳脫非 ASCII 與不可列印字元,沒有保證自動處理 < 這種 ASCII 字元;HTML Standard 則明定 < 會讓腳本資料進入另一個解析狀態。
textContent 在這裡只負責把已取得的字串顯示成文字,不負責修補前面腳本來源中的注入問題。OWASP 將它列為顯示文字時可使用的 DOM 寫入方式。
我從專案根目錄執行:
python3 day-12/run_experiment.py
程式暫時在 127.0.0.1 啟動網站,依序送出 3 組探針,再保存完整 HTML 回應、腳本內容、瀏覽器觀察與 SHA-256。這次環境是 Python 3.14.4 與 Google Chrome 153.0.8010.36,3 個頁面都回傳 200 和 text/html。
弱點頁的原始回應中,賦值已經變成:
const query = '';document.documentElement.setAttribute('data-day12-xss','day12-f83e931be915e42b');alert(1);//';
第一個單引號把 query 截成空字串;之後的 DOM 寫入和 alert(1) 變成同一行的獨立程式碼,// 則吃掉原本用來結束字串的部分。原始回應只能證明這段文字確實進入腳本;瀏覽器觀察才確認它有沒有執行。
Chrome 捕捉到訊息為 1 的 alert,也在 <html> 讀回 data-day12-xss="day12-f83e931be915e42b"。畫面腳本後續正常執行,而 data-query-value 是空字串,符合前面字串被截斷的結果。因此,這個本機頁面的本次固定探針分類為 confirmed_xss。
編碼頁的原始賦值仍看得到探針文字,但它待在一個完整的雙引號字串常值內:
const query = "';document.documentElement.setAttribute('data-day12-xss','day12-26f3455c91050a86');alert(1);//";
Chrome 讀回的 data-query-value 與送出的整段探針相同,data-script-ran 也是 true。這表示頁面的正常腳本有執行,並將輸入當作資料顯示;本次沒有觀察到探針的 alert 或 DOM 旗標。分類是 reflected_but_not_executed。單看「腳本有執行」或「回應有 alert(1) 文字」,都會漏掉這個差別。
我另送出含 </script><script>... 的測試值,檢查另一層 HTML 邊界。編碼頁的原始回應把輸入中的 < 寫成 \u003C,沒有原樣出現輸入的 </script><script>;Chrome 仍把完整測試值讀成 data-query-value,也沒有觀察到探針執行。這項測試只驗證編碼頁在本次組合下的行為,不把它推論成所有嵌入腳本做法都安全。
不反射頁使用固定字串。原始回應沒有本次識別碼,DOM 中的 data-query-value 是「固定內容」,也沒有 alert 或探針旗標。它屬於 not_reflected,不是「反射後沒有執行」。
| 組別 | 原始腳本含本次探針 | DOM 查詢值 | alert(1) 與 DOM 旗標 |
分類 |
|---|---|---|---|---|
| 弱點頁 | 是,且原始單引號被跳出 | 空字串 | 兩者都有 | confirmed_xss |
| 編碼頁 | 是,留在完整字串內 | 完整探針 | 兩者都沒有 | reflected_but_not_executed |
| 不反射頁 | 否 | 固定內容 | 兩者都沒有 | not_reflected |
三組原始回應與 JSON、雜湊及摘要都保存在 day-12/results/。我也執行了 Day 12 的 6 個瀏覽器整合測試,以及 Day 10 的 6 個、Day 6 的 5 個回歸測試,全部通過。測試除了三個端點,也涵蓋識別碼限制、本機目標限制、既有查詢參數,以及 </script> 文字進入編碼頁時的行為。
今天直接確認的是:在這個本機、沒有設定 CSP 的行內腳本裡,q 原樣進入單引號字串時,本次探針能跳出並執行;經過此處的字串序列化及 HTML 腳本邊界處理後,同一探針只作為資料;固定內容頁沒有反射本次識別碼。
這些結果不能代替其他 JavaScript 語法位置、模板字面值、DOM 型或儲存型 XSS 的檢查,也不能替真實網站指定風險等級。今天沒有把結果交給 AI 產生報告;先讓底層證據清楚區分「反射到腳本」「正常腳本執行」與「探針程式碼執行」,後續報告才有可靠的輸入。
那就…
明天見!