跨站腳本(Cross-Site Scripting,XSS)發生在網站把使用者輸入的特殊字元未經 sanitize 直接渲染,導致瀏覽器執行攻擊者注入的 JavaScript。最經典案例是 2005 年 10 月由 Samy Kamkar(薩米·卡姆卡)在 Myspace 上釋放的 Samy Worm:他在自己的 profile 上注入 JavaScript,任何登入的訪客一打開他的頁面,瀏覽器就會自動把訪客變成他的好友、改寫訪客 profile 寫上「but most of all, samy is my hero」並繼續複製到其他頁面,引發跨使用者擴散。
這條蠕蟲讓 Kamkar 被搜索住處並認罪——XSS 看似只是「彈出一個 alert」,但放大效應可以是全站性災難。
危險字元與 sanitize#
幾個會打開攻擊面的字元:
- 雙引號(
") - 單引號(
') - 角括號(
<、>)
正確 sanitize 後應該以 HTML 實體呈現:
| 原字元 | HTML 實體 |
|---|---|
" | " 或 " |
' | ' 或 ' |
< | < 或 < |
> | > 或 > |
最直白的測試 payload:
<script>
alert(document.domain);
</script>alert 跳出視窗顯示當前 DOM 的 domain——驗證 XSS 在哪個 origin 上執行很關鍵。
同源政策(SOP)與 XSS 的影響#
瀏覽器以同源政策(Same-Origin Policy,SOP) 限制不同來源(origin)之間的互動。Origin 由 協定 + 主機 + 連接埠 三者構成(IE 例外不看 port):
| URL | 同源於 http://www.example.com/? | 原因 |
|---|---|---|
http://www.example.com/countries | 是 | — |
https://www.example.com/countries | 否 | 協定不同 |
http://store.example.com/countries | 否 | 主機不同 |
http://www.example.com:8080/countries | 否 | 連接埠不同 |
兩個特殊情況:
about:blank與javascript:URI 繼承呼叫者的 origin。所以javascript:alert(document.domain)在www.example.com上開啟時,alert 會顯示www.example.com。
評估 XSS 影響時的判斷點:
- 受害網站是否在敏感 Cookie 上設了
httponly?沒設 → 可竊取 session - 即便 Cookie 拿不到,是否能透過 DOM 取使用者資料、代為操作?
- XSS 執行時的
document.domain是不是受害網站本身?若在 sandboxed iframe 裡的 alert,可能無傷大雅
注入點不只 <script>#
並非每個地方都能直接塞 <script>。以下是常見的「在屬性內或變數內」注入:
屬性內注入#
<input type="text" name="username" value="hacker" width="50px" />把 value 改成 hacker" onfocus=alert(document.cookie) autofocus ":
<input type="text" name="username" value="hacker" onfocus=alert(document.cookie)
autofocus "" width=50px>autofocus 讓欄位載入時自動取得焦點,onfocus 立刻觸發 alert。
autofocus在隱藏欄位失效;多個autofocus元素時依瀏覽器取第一個或最後一個。
<script> 內變數注入#
<script>
var name = "hacker";
</script>如果 hacker 可控,把它換成 hacker';alert(document.cookie);':
<script>
var name = "hacker";
alert(document.cookie);
("");
</script>' 關閉變數、; 結束句子、注入 alert,最後 ' 確保語法仍然合法。
Cure53 維護的 https://html5sec.org/ ↗ 是 XSS payload 的權威參考。
XSS 的分類#
Reflected XSS(反射型)#
惡意 payload 透過單次 HTTP 請求送出後立即被回應渲染、執行——payload 不被儲存。Chrome、IE、Safari 早期內建 XSS Auditor 試圖阻擋(Microsoft 已於 2018 年宣布在 Edge 退役)。Auditor 不時被繞過,相關技巧可參考:

Figure 7-1: 被 Google Chrome XSS Auditor 阻擋的頁面
- FileDescriptor 的部落格:https://blog.innerht.ml/the-misunderstood-x-xss-protection/ ↗
- Masato Kinugawa 的 cheat sheet:https://github.com/masatokinugawa/filterbypass/wiki/Browser's-XSS-Filter-Bypass-Cheat-Sheet/ ↗
Stored XSS(儲存型)#
payload 被儲存到伺服器,渲染時才執行。風險最高——可能在多個頁面、不同時間爆發。
子分類#
- DOM-based XSS:JavaScript 操作既有 DOM 時引入了惡意輸入,例如:造訪
<script> document.getElementById("name").innerHTML = location.hash.split("#")[1]; </script>www.example.com/hi#<img src=x onerror=alert(document.domain)>即可觸發 - Blind XSS:payload 在駭客看不到的位置執行(例如管理員後台),需用工具驗證。Matthew Bryant 的 XSSHunter ↗ 是經典工具
- Self XSS:只能影響自己的 XSS,多數計畫不付錢;要把它變武器需結合 login/logout CSRF 等手法(Jack Whitton 在 Uber 的案例:https://whitton.io/articles/uber-turning-self-xss-into-good-xss/ ↗)
案例 1:Shopify Wholesale#
- 難度:低
- URL:
wholesale.shopify.com/- 回報來源:https://hackerone.com/reports/106293/ ↗
- 回報日期:2015-12-21
- 獎金:$500
漏洞描述#
Shopify wholesale 首頁的搜尋框,輸入會被原樣插入既有的 <script> 標籤內。HTML 字元被編碼,但放在 JavaScript 字串字面值內,依舊可破。
研究者送入:
test';alert('XSS');'伺服器產生:
var search_term = 'test';alert('XSS');'';Takeaways#
永遠檢視原始碼(
view-source:URL)確認 payload 落點。落在 HTML 還是落在<script>內字串,所需的 payload 完全不同。

Figure 7-2: https://nostarch.com/ 的頁面原始碼
案例 2:Shopify Currency Formatting#
- 難度:低
- URL:
<YOURSITE>.myshopify.com/admin/settings/general/- 回報來源:https://hackerone.com/reports/104359/ ↗
- 回報日期:2015-12-09
- 獎金:$1,000
漏洞描述#
商店設定的「貨幣格式」欄位輸入未經 sanitize 就出現在「社群媒體銷售通路」頁面。Shopify 用 Liquid 範本引擎,正常值是 ${{amount}};攻擊者改成:

Figure 7-3: 回報當下的 Shopify 貨幣設定頁
${{amount}}"><img src=x onerror=alert(document.domain)>"> 關閉既有 HTML 標籤,<img onerror> 觸發 XSS。XSS 不是在貨幣設定頁觸發,而是在另一位管理員打開銷售通路頁時觸發。
Takeaways#
XSS payload 不一定立即執行。要把所有「該值可能被渲染」的位置都走過一遍,才知道真正的觸發場景。
案例 3:Yahoo! Mail Stored XSS#
- 難度:中
- URL:Yahoo! Mail
- 回報來源:https://klikki.fi/adv/yahoo.html ↗
- 回報日期:2015-12-26
- 獎金:$10,000
漏洞描述#
Yahoo! Mail 編輯器允許嵌入 <img> 標籤,且把 onload、onerror 等 JS 屬性過濾掉。但 Jouko Pynnonen 利用了 Boolean 屬性的解析瑕疵:
<input type="checkbox" checked="hello" name="check box" />Yahoo 把 "hello" 拿掉但保留等號:
<input type="checkbox" checked="NAME" ="check box" />HTML 規範允許等號旁邊有空白,於是瀏覽器解析成 CHECKED 的值是 NAME="check、第三個屬性叫 box——整個結構錯位。Pynnonen 利用此邏輯:
<img
ismap="xxx"
itemtype="yyy style=width:100%;height:100%;position:fixed;left:0px;top:0px; onmouseover=alert(/XSS/)//"
/>Yahoo! 過濾後變成:
<img ismap= itemtype='yyy'
style=width:100%;height:100%;position:fixed;left:0px;top:0px;
onmouseover=alert(/XSS/)//><img> 鋪滿整個瀏覽器視窗,使用者一移動滑鼠 onmouseover 就觸發 XSS。
Takeaways#
當網站「修改」使用者輸入做 sanitize(而非編碼或轉義),思考開發者隱含的假設:兩個
src屬性會怎樣?空白被換成/會怎樣?Boolean 屬性帶值會怎樣?這些角落很容易藏漏洞。
案例 4:Google Image Search#
- 難度:中
- URL:
images.google.com/- 回報來源:https://mahmoudsec.blogspot.com/2015/09/how-i-found-xss-vulnerability-in-google.html ↗
- 回報日期:2015-09-12
- 獎金:未公開
漏洞描述#
Mahmoud Jamal 注意到圖片搜尋結果的 URL:
http://www.google.com/imgres?imgurl=https://lh3.googleuser.com/...imgurl 直接被當作 <a href>。他改成 javascript:alert(1)——href 也跟著變:
<a href="javascript:alert(1)">...</a>
javascript:URI 不需特殊字元繞過,且因為繼承父頁 origin,可在 google.com 的上下文中執行。
但點擊時 Google 的 onmousedown 又會把 URL 過濾。Jamal 換了招——按 Tab 把焦點移到 View Image 後按 Enter,不經過滑鼠點擊,過濾沒觸發,XSS 成功。
Takeaways#
只要 URL 參數會反映到頁面,就值得測;如果結果落在
href中,可以用javascript:URI 繞過字元過濾。連 Google 這種大公司都會有這種漏洞,別因為對方有名氣就放棄嘗試。
案例 5:Google Tag Manager Stored XSS#
- 難度:中
- URL:
tagmanager.google.com/- 回報來源:https://blog.it-securityguard.com/bugbounty-the-5000-google-xss/ ↗
- 回報日期:2014-10-31
- 獎金:$5,000
漏洞描述#
Patrik Fehrenbach 嘗試在 Google Tag Manager 的 Web 表單注入 #"><img src=/ onerror=alert(3)> 都被 sanitize。但 Tag Manager 還支援上傳 JSON 檔批次匯入:
"data": {
"name": "#\"><img src=/ onerror=alert(3)>",
"type": "AUTO_EVENT_VAR",
"autoEventVarMacro": {
"varType": "HISTORY_NEW_URL_FRAGMENT"
}
}Google 在「送入時 sanitize」而非「渲染時 sanitize」——上傳路徑被忘記了。
Takeaways#
- 找替代輸入路徑:Web 表單、JSON 上傳、API、行動 App 等
- 最佳實務是「在渲染時 sanitize」而非「在送入時 sanitize」——前者較不易因新增輸入入口而遺漏
案例 6:United Airlines XSS#
- 難度:高
- URL:
checkin.united.com/- 回報來源:http://strukt93.blogspot.jp/2016/07/united-to-xss-united.html ↗
- 回報日期:2016 年 7 月
- 獎金:未公開
漏洞描述#
Mustafa Hasan 發現 checkin.united.com 的 SID 參數直接寫入 HTML,但他注入 "><svg onload=confirm(1)> 並未觸發。原因是站上載入了一段「XSS 防禦」JavaScript,把 alert、confirm、prompt、document.write 等函式都用 proxy 重寫成 no-op:
XSSObject.proxy(window, "alert", "window.alert", false);
XSSObject.proxy(window, "confirm", "window.confirm", false);
XSSObject.proxy(window, "prompt", "window.prompt", false);
XSSObject.proxy(document, "write", "document.write", false);Hasan 注意到名單漏了 writeln。他與 Rodolfo Assis 合作後產出:
";}{document.writeln(decodeURI(location.hash))-"#<iframe src=javascript:alert(document.domain)><iframe>關鍵設計:
";}關閉既有 JS 區塊{document.writeln(decodeURI(location.hash))把 URL 的 fragment 解碼後寫入 DOM-"修補語法#<iframe src=javascript:alert(document.domain)>在 fragment 內注入<iframe>,src 用javascript:URI
<iframe> 是「全新 HTML 文件」,不會載入父頁的 XSS Filter;又因為 javascript: 繼承父 origin,alert 顯示 www.united.com——XSS 確認成立。
Takeaways#
- 持續挖掘:payload 沒觸發時,去看為什麼
- 看到黑名單就要警覺:黑名單是漏洞的明燈,因為總有一個函式被忘記列入
- JavaScript 知識是必備:複雜 XSS 的驗證需要靈活操作 DOM 與 prototype
章末總結#
- XSS 是長期高發的漏洞,最簡單的測試 payload 是
<img src=x onerror=alert(document.domain)> - 影響取決於:reflected vs stored、是否能讀 Cookie、執行時的 origin、能否操作 DOM
- 常被忽略的測試點:
- 「修改式」sanitize 的邊界(Boolean 屬性帶值、雙重屬性、空白被替換等)
- 替代輸入路徑(JSON、API、檔案上傳、行動 App)
- URL 參數被反映到
href→ 試javascript:URI - 黑名單防禦——找漏掉的函式或屬性
- payload 不一定立刻執行,要把該值可能出現的所有頁面走過一遍