Cross-Site Scripting Attacks#

概述#

當 web server 安全無虞時,駭客的下一個目標就是瀏覽器。瀏覽器會忠實地執行網頁中出現的任何 JavaScript 程式碼,因此如果攻擊者能將惡意 JavaScript 注入使用者的瀏覽器,就能造成嚴重危害。這種攻擊稱為 Cross-Site Scripting (XSS)

透過 XSS,攻擊者可以:

  • 讀取或修改網頁的任何部分
  • 竊取登入憑證或信用卡號等敏感資訊
  • 讀取 HTTP session 資訊,劫持使用者的 session(session hijacking)
  • 將使用者重導到惡意網站

本章介紹三種最常見的 XSS 攻擊類型及其防禦方式。

Stored Cross-Site Scripting Attacks#

Stored XSS(儲存型 XSS)是指攻擊者將惡意 JavaScript 寫入資料庫,當其他使用者瀏覽包含該內容的頁面時,惡意程式碼就會在受害者的瀏覽器中執行。

攻擊方式#

  • 網站通常從資料庫取出內容來產生 HTML(例如產品資訊、使用者對話)
  • 任何來自資料庫的頁面內容都是潛在的攻擊向量
  • 攻擊者嘗試將 JavaScript 程式碼注入資料庫,使 web server 在渲染 HTML 時將其輸出

典型範例: 在一個論壇網站中,攻擊者在留言中寫入 <script>alert('Your croissants are limp and sad.')</script>。如果網站在建構 HTML 時未適當 escape,這段 <script> 標籤就會被寫入其他使用者的瀏覽器並執行。

不只是留言區域有風險。任何使用者可控制的內容都是潛在的攻擊途徑,包括使用者名稱、個人檔案頁面、線上評論等。

Figure 7-1: 攻擊者透過留言注入 JavaScript

緩解措施 1:Escape HTML 字元#

防止 stored XSS 的關鍵是 escape(跳脫)所有來自資料庫的動態內容,使瀏覽器將其視為 HTML 的文字內容而非原始 HTML 標籤。

Entity encoding 對照表:

字元Entity Encoding
"&quot;
&&amp;
'&apos;
<&lt;
>&gt;

經過 escape 後,瀏覽器會在建構 DOM 之後才將 escaped 字元轉換回原始字元,因此不會執行 <script> 標籤。

Figure 7-2: 攻擊者的 script 標籤被寫入受害者的頁面

現代 web framework 的 template engine 預設會自動 escape 動態內容。例如 Embedded Ruby (ERB) template 中,<%= comment %> 語法會自動 escape。要輸出原始 HTML 則需要明確呼叫 raw 函式(<%= raw comment %>),這樣做會有 XSS 風險。

在 code review 時要特別注意:

  • 確認動態內容在 template 中被安全 escape
  • 檢查是否有 helper function 或方法會建構原始 HTML 並注入 template

緩解措施 2:實作 Content Security Policy#

Content Security Policy (CSP) 讓你可以控制網站上 JavaScript 的執行方式。XSS 攻擊通常依賴在 <html> 標籤中注入 <script> 標籤(即 inline JavaScript)。

透過設定 CSP,你可以告訴瀏覽器永遠不要執行 inline JavaScript,只執行透過 src 屬性匯入的外部腳本。

透過 HTTP response header 設定:

Content-Security-Policy: script-src 'self' https://apis.google.com

透過 HTML <meta> 標籤設定:

<meta
  http-equiv="Content-Security-Policy"
  content="script-src 'self' https://apis.google.com"
/>

這個政策表示瀏覽器只會從同一網域('self')和 apis.google.com 載入 JavaScript,inline JavaScript 不會被執行。若要允許 inline JavaScript,需加入 unsafe-inline 關鍵字。

禁止 inline JavaScript 會迫使開發團隊將所有 JavaScript 移到外部檔案,這其實是現代 web 開發的最佳實踐。如果要逐步遷移,可以先使用 violation report 模式:

Content-Security-Policy-Report-Only: script-src 'self'; report-uri https://example.com/csr-reports

這樣瀏覽器會回報違規情況而非阻擋執行,方便團隊找出需要修改的頁面。

應同時使用 escape HTMLCSP 作為多層防禦(defense in depth),因為攻擊者很難同時找到未 escape 的內容並且將惡意腳本放到白名單網域上。

Reflected Cross-Site Scripting Attacks#

Reflected XSS(反射型 XSS)與 stored XSS 不同,惡意 JavaScript 不是存在資料庫中,而是透過 HTTP request 注入。當網站將 HTTP request 的一部分顯示在渲染的頁面上時,就可能受到此類攻擊。

攻擊方式#

幾乎所有網站都會將 HTTP request 的部分內容顯示在渲染的 HTML 中。例如搜尋頁面會將搜尋詞顯示在搜尋框上方。

攻擊流程:

  1. 攻擊者將 URL 中的搜尋參數替換為惡意 JavaScript
  2. 將這個 URL 透過 email 或留言發送給受害者
  3. 受害者點擊連結後,瀏覽器開啟該 URL
  4. 伺服器將惡意程式碼「反射」回頁面中執行

即使是大型公司也可能出現 reflected XSS 漏洞。駭客曾在 Google Apps 管理介面(admin.google.com)發現此類漏洞。

常見目標#

  • 搜尋頁面錯誤頁面,因為它們通常會將 query string 的部分內容顯示給使用者

Stored vs. Reflected XSS 比較#

  • Stored XSS 通常危害更大:一段注入資料庫的惡意 JavaScript 可以反覆攻擊所有瀏覽該頁面的使用者
  • Reflected XSS 更常見,因為實作門檻較低

緩解措施:Escape HTTP Request 中的動態內容#

防禦方式與 stored XSS 相同 — escape 網站插入 HTML 頁面的動態內容中的控制字元。無論動態內容來自資料庫還是 HTTP request,都需要以相同方式 escape。

Code review 時經常忽略 reflected XSS 漏洞,因為開發者通常只關注 stored XSS。確保團隊了解這個風險並在審查時注意。

DOM-Based Cross-Site Scripting Attacks#

DOM-based XSS 是相對較新的攻擊形式,隨著豐富的 client-side JavaScript framework 興起而出現。攻擊者透過 URI fragment 將惡意 JavaScript 注入使用者的網頁。

URI Fragment 的運作方式#

URL 的結構為:Protocol://Domain/Path?QueryString#URIFragment

  • URI fragment 是 URL 中 # 號後面的部分
  • 瀏覽器使用它進行頁面內導航(跳到指定 id 的 HTML 元素)
  • Single-page apps(如 Angular、Vue.js、React)使用 URI fragment 記錄和恢復狀態(例如 infinite scrolling 的捲動位置)

關鍵特性#

瀏覽器設計上不會將 URI fragment 發送給伺服器。這意味著:

  • URI fragment 不會出現在任何 server-side 程式碼中
  • 保護 server-side 程式碼無法防禦 DOM-based XSS
  • 攻擊完全發生在 client side,無法透過檢查 web server 日誌來偵測

如果 client-side JavaScript 讀取 URI fragment 的內容並將其未經 escape 地直接寫入 DOM,攻擊者可以:

  1. 建構一個 URI fragment 中含有惡意 JavaScript 的 URL
  2. 誘騙使用者訪問該 URL
  3. 惡意程式碼在使用者的瀏覽器中執行

緩解措施:Escape URI Fragment 中的動態內容#

  • 任何從 URI fragment 取得資料並建構 HTML 的 JavaScript 程式碼都容易受到 DOM-based XSS 攻擊
  • 必須在將 URI fragment 的值插入 HTML 之前進行 escape,如同處理 server-side 程式碼一樣
  • 現代 JavaScript template framework 已意識到此風險,不鼓勵在程式碼中建構原始 HTML

例如在 React 中,要寫入未 escape 的 HTML 需要明確使用名為 dangerouslySetInnerHTML 的函式:

function writeSomeHTML() {
  return { __html: "First &middot; Second" };
}
function MyComponent() {
  return <div dangerouslySetInnerHTML={writeSomeHTML()} />;
}

如果你的 client-side JavaScript 程式碼很複雜,考慮遷移到現代 JavaScript framework(如 React),它們讓程式碼更易管理,安全考量也更明確。同時務必設定適當的 content security policy。

總結#

  • XSS 攻擊是指攻擊者將 JavaScript 注入網站頁面,在使用者瀏覽時執行
  • 攻擊者通常透過三種途徑注入惡意 JavaScript:資料庫(stored)、HTTP request(reflected)、URI fragment(DOM-based)
  • 防禦的核心是 escape 動態內容中的 HTML 控制字元
  • 設定 Content Security Policy 防止 inline JavaScript 執行,作為額外的防禦層