iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Build on Google AI

30 天打造 AI Web Security Agent:從 Google AI Studio 到 Gemini Agent系列 第 12 篇

Day 12|輸入進了 JavaScript 字串,反射與執行還是一回事嗎?

  • 分享至 

  • xImage
  •  

前言

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 個對照頁

我讓 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 產生報告;先讓底層證據清楚區分「反射到腳本」「正常腳本執行」與「探針程式碼執行」,後續報告才有可靠的輸入。

那就…
明天見!


上一篇
Day 11|把 HTML 屬性的三種結果交給 AI,報告能保留差異嗎?
系列文
30 天打造 AI Web Security Agent:從 Google AI Studio 到 Gemini Agent 共 12 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言