題幹與適用情境
一個 React 社群應用程式有三條渲染路徑:
- 一般留言儲存在資料庫中,頁面需要原樣顯示文字。
- 管理員公告允許使用有限的富文字:段落、強調、清單與 HTTPS 連結。
- 搜尋頁從
location.search讀取q,在前端顯示「關於……的結果」。
審查發現一般留言和公告預覽進入了 dangerouslySetInnerHTML 或 innerHTML,搜尋提示也透過 串接 HTML 字串渲染。請找出所有從資料來源到注入點的路徑,區分攻擊資料的傳播方式,依輸出 脈絡選擇編碼或淨化方案,並加入能降低漏網注入點影響的控制。
基礎回答聚焦瀏覽器渲染與前端責任。伺服器端驗證、授權與內容儲存仍是必要邊界,但它們不會 讓危險的 DOM 注入點自動變安全。這題的決定性能力是理解瀏覽器解析、框架逃生口、DOM 注入點、 CSP 與可驗證的渲染契約,因此歸入 frontend。
儲存型、反射型與 DOM 型是有用標籤,但它們關注的維度不同。儲存型和反射型描述攻擊者資料 如何到達受害者;DOM 型描述前端 JavaScript 如何把資料送進可執行的注入點。儲存或反射的 資料也可能最後在 DOM 注入點執行,因此不能把三者視為完全互斥的分類。
面試官考察點
第一個訊號是候選人能否先畫資料流,再列安全回應標頭。強回答會識別資料庫欄位、URL 參數、 片段、postMessage 與第三方回應等資料來源,也會識別 innerHTML、outerHTML、 insertAdjacentHTML、document.write、eval 與字串計時器等注入點。核心問題是: 攻擊者可控資料能否進入把字串解讀為標記、腳本或腳本 URL 的解析器?
第二個訊號是能否依資料的預期型別選擇防禦。一般留言屬於文字,應進入文字注入點或一般 JSX 插值。公告確實需要 HTML,實體編碼會破壞功能,因此必須在可信渲染邊界前使用持續維護、 策略收斂的 HTML 淨化器。URL 還要在正確輸出脈絡之外,獨立驗證協定與目的地。
第三個訊號是能否說清楚框架邊界。React 會跳脫一般字串插值,dangerouslySetInnerHTML 則是 明確的逃生口。框架無法挽救未處理的原始 HTML,也無法自動判斷每個交給危險屬性的 javascript: 或 data: URL 是否符合產品規則。
最後一個訊號是分層驗證。CSP 可以限制腳本執行,Trusted Types 可以讓部分注入點拒絕一般 字串,但它們都無法修正過寬的淨化策略或直接回傳原值的策略函式。強回答會刪除不必要的 注入點、收斂唯一保留的注入點、部署瀏覽器端控制,再用資料流審查和惡意案例證明結果。
回答前需要釐清的問題
- 哪些欄位是文字,哪些欄位確實需要 HTML? 如果全部是文字,刪除所有原始 HTML 渲染。
如果公告需要格式,先精確定義允許的標籤、屬性與 URL,再選擇淨化器。
- 誰能發布公告? 只有管理員可寫會降低暴露面,但內容仍應按不可信資料處理。管理員工作階段
被竊、匯入工具遭入侵、資料遷移錯誤或 API 缺陷都可能寫入惡意內容。
- 公告連結能否離開本站? 基礎方案只允許 HTTPS。若允許郵件、自訂協定、圖片、影片或
內嵌頁面,淨化策略與隔離方式都要擴大。
- 目前在哪裡淨化? 寫入時淨化可以提早拒絕惡意輸入,但渲染邊界仍要保證:所有進入原始
HTML 注入點的值都經過目前策略處理。
- 需要支援哪些瀏覽器? CSP 具有廣泛價值;Trusted Types 的支援範圍和上線方式必須依真實
瀏覽器矩陣確認,未支援的用戶端仍依賴安全注入點與 HTML 淨化。
- 哪些第三方腳本必須保留? 腳本集合越小、稽核越清楚,嚴格腳本策略越容易。標籤管理器與
行內片段會改變 CSP 遷移方案,也會擴大成功 XSS 的影響。
- 怎樣才算完成修正? 成功標準應包含迴歸測試、CSP 違規監控、淨化器升級責任、策略變更
責任,以及發現新增注入點的方法。
30 秒回答框架
「我會先盤點所有不可信資料來源與可執行注入點,再依資料的預期型別逐條修正。一般留言和 搜尋詞都是文字,用 React 一般插值或 textContent 渲染,完全避開 HTML 解析。公告確實需要 有限 HTML,所以我會定義允許清單,讓內容經過持續維護的淨化器,並只保留一個經過審查的富 文字元件;連結還要驗證協定。我會刪除其他 innerHTML,先以 report-only 上線基於 nonce 或 hash 的 CSP,再逐步強制執行;瀏覽器矩陣支援時,用 Trusted Types 拒絕 DOM 注入點的原始 字串。最後用儲存資料、URL、畸形標籤、事件屬性、SVG 與腳本 URL 案例測試,同時確認允許的 格式仍正常。」
分步深入解答
第一步:畫出所有資料來源到解析注入點的路徑
先做一張小型資料流表,不要從背攻擊字串開始:
| 路徑 | 攻擊者可控資料來源 | 目前注入點 | 預期型別 | 必要修改 | |---|---|---|---|---| | 留言卡片 | 資料庫留言內容 | 原始 HTML 渲染 | 文字 | JSX 插值或 textContent | | 管理員公告 | 資料庫公告內容 | 原始 HTML 渲染 | 有限 HTML | 允許清單淨化後進入唯一注入點 | | 搜尋提示 | location.search | HTML 字串串接 | 文字 | textContent 或 React 文字子節點 | | 公告連結 | 富文字中的 href | URL 屬性 | HTTPS URL | 解析協定、驗證後再保留屬性 |
程式碼審查既要搜尋直接注入點,也要搜尋它們的封裝。變數名稱叫 safeHtml 無法證明安全, 必須追到產生它的轉換函式。還要檢查 Markdown 渲染器、富文字編輯器、預覽元件、含標記的 在地化訊息、DOM 解析呼叫、SVG、範本工具、分析腳本,以及把 URL 或訊息資料寫入頁面的程式碼。
路徑確定後再分類:
- 惡意留言寫入資料庫並顯示給其他使用者,屬於儲存型傳播。
- 請求參數被伺服器立即放入回應,屬於反射型傳播。
- 前端讀取查詢參數或片段,再送入
innerHTML,屬於 DOM 型執行路徑。
這些標籤有助於評估暴露面與事件處理;修正選擇仍由資料來源、輸出脈絡與注入點決定。
第二步:讓文字始終以文字渲染
一般留言和搜尋提示不需要標記,直接使用 React 文字子節點:
function CommentBody({ body }: { body: string }) {
return <p>{body}</p>
}
function SearchSummary({ query }: { query: string }) {
return <p>關於「{query}」的結果</p>
}離開 React 時使用文字注入點:
summaryNode.textContent = query瀏覽器此時把資料當文字,不會重新依 HTML 解析。不要用正規表示式刪除 script 字樣後繼續 賦值給 innerHTML;HTML 的元素、屬性、編碼與畸形輸入恢復規則很多,正規表示式無法形成 可靠解析邊界。
脈絡仍然重要。文字位置安全,不代表同一個值放進事件處理器、腳本區塊、CSS 規則或 URL 後 仍然安全。避免把不可信值放入可執行脈絡。確實需要寫入安全屬性時,固定屬性名稱,並在框架 屬性或 setAttribute 前完成該屬性所需的驗證。
第三步:隔離唯一需要 HTML 的功能
公告需要保留有限格式,不能改成純文字。先定義策略:
- 允許標籤:段落、粗體、強調、無序和有序清單、清單項目、連結。
- 允許屬性:只有
href、title,以及渲染器統一加入的連結屬性。 - 基礎情境允許的連結協定:HTTPS。
- 禁止內容:腳本、事件屬性、行內樣式、內嵌頁面、表單、SVG 與任意媒體。
接著把原始 HTML 注入點集中到一個元件:
function RichAnnouncement({ dirtyHtml }: { dirtyHtml: string }) {
const cleanHtml = sanitizeRichText(dirtyHtml, RICH_TEXT_POLICY)
return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />
}sanitizeRichText 代表按上述策略設定、基於解析器且持續維護的淨化器。OWASP 推薦 DOMPurify 作為可選實作之一。安全性來自淨化器及其設定,變數名稱或 TypeScript 型別本身無法提供防護。
淨化應成為進入受審渲染邊界前的最後轉換。寫入時淨化可以增加一道檢查,但單獨依賴它會遺漏 舊資料、匯入、遷移、備用 API 與策略變更。如果為了效能儲存淨化結果,應記錄淨化器和策略 版本,方便安全更新後重新處理歷史內容。
淨化後不要再用字串串接修改 HTML。後續加入連結、醒目提示或包裝標籤可能重新引入可執行 結構。可信 UI 應建立在原始 HTML 區域之外;確實需要修改最終內容時,再經過一次完整淨化。
第四步:把 URL 當成結構化輸入
HTML 淨化必須涵蓋連結屬性,產品策略也要明確。把候選 URL 相對已知基底網址解析,讀取 正規化後的協定,只保留功能真正需要的協定。本題基礎情境只允許 HTTPS。
對危險 URL 做編碼,無法讓其協定自動符合要求。一個值可以正確編碼進 href,同時仍使用 腳本協定。URL 合法性和 HTML 屬性脈絡是兩項獨立檢查。外部連結在新分頁開啟時,由渲染器 統一加入合適的關聯屬性;這能限制 opener 權限,但不能取代 XSS 防禦。
完全禁止使用者控制腳本 URL。eval、Function、字串形式的 setTimeout、字串形式的 setInterval 與動態腳本網址都不應接收不可信值。這些路徑需要刪除或改成固定對照,不能交給 通用淨化器兜底。
第五步:把 CSP 當作第二道防線
如果仍有注入點漏網,CSP 可以限制腳本執行。優先透過回應標頭下發,並依應用程式真實資源 設計策略。簡化方向如下:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{每個回應獨立的隨機值}';
object-src 'none';
base-uri 'none'nonce 必須不可預測,且每個回應唯一,只提供給已核准腳本。穩定行內內容也可評估 hash 策略。 不要為了消除違規紀錄加入寬泛網域或 unsafe-inline,這會削弱策略邊界。
先使用 Content-Security-Policy-Report-Only 收集違規,移除意外行內程式碼,再開啟強制策略。 report-only 能提供上線證據,但不會阻止執行。CSP 始終屬於縱深防禦:允許的腳本仍擁有頁面 權限,不同瀏覽器對功能的支援有差異,過寬策略也可能讓原漏洞繼續執行。
第六步:用 Trusted Types 約束 DOM 注入
瀏覽器支援時加入以下 CSP 指令:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types app-rich-text開啟後,受涵蓋的 DOM 注入點會拒絕一般字串。應用程式只建立指定名稱的策略,該策略把 HTML 建立委託給核准淨化器。原本靜默發生的危險賦值會變成可見例外,新引入的原始字串注入點也更 容易在測試和監控中暴露。
如果策略直接回傳輸入,控制就失去意義。寬泛的預設策略也可能把所有字串自動轉換,掩蓋舊 路徑。遷移期可以暫時使用預設策略收集診斷資訊,之後讓呼叫端改用明確可信值,並刪除不必要 的注入點和相容路徑。
Trusted Types 不能涵蓋所有程式碼執行方式,舊瀏覽器也可能不支援強制機制。基線仍然是安全 渲染、收斂淨化、URL 驗證與移除可執行字串 API。
第七步:降低渲染函式之外的影響
工作階段 Cookie 通常應設定 HttpOnly、Secure 與適當的 SameSite。HttpOnly 能阻止 注入腳本直接讀取 Cookie,但活躍 XSS 仍可代表使用者送出請求,或讀取頁面可存取資料。Cookie 屬性只能降低影響,不能關閉注入路徑。
每個敏感操作都必須由伺服器授權,即使前端隱藏了對應按鈕。伺服器邊界依需要驗證或淨化輸入, 儲存的資料仍標記為不可信。第三方腳本擁有頁面權限,應減少數量、限制到必要路由、稽核更新, 並納入 CSP 設計。
第八步:同時驗證安全性與允許格式
建立涵蓋不同解析路徑的案例集:
<img src=x onerror=alert(1)>
<a href="javascript:alert(1)">開啟</a>
<svg onload=alert(1)></svg>
"><script>alert(1)</script>
畸形標籤與混合字元編碼在隔離測試環境使用無破壞性的探針,不要在一般帳號或正式環境執行真實攻擊行為。驗證:
- 一般留言逐字顯示,且不會建立輸入中包含的元素。
- 搜尋參數在一般導覽、編碼 URL、歷史紀錄和 hydration 後仍是文字。
- 公告保留允許標籤,同時刪除腳本、事件屬性、樣式、SVG、內嵌頁面與危險 URL。
- 淨化結果抵達原始 HTML 注入點前沒有再次被修改。
- 理解 CSP report-only 遙測後開啟強制策略,刻意加入的測試注入點會被阻擋。
- Trusted Types 強制模式會拒絕原始字串,只接受核准策略產生的淨化結果。
- 既有富文字案例、連結、螢幕閱讀器結構與複製貼上行為仍正常。
加入新增注入點的靜態審查、淨化策略單元測試、每條渲染路徑的整合測試與瀏覽器矩陣測試。 持續升級淨化器;瀏覽器、框架、淨化器、編輯器或策略變更時,重新執行惡意案例集。
高品質示範回答
「我會先識別不可信資料來源和負責解析它們的瀏覽器 API。資料庫留言、公告 HTML 和 location.search 中的 q 都是資料來源;innerHTML、dangerouslySetInnerHTML 以及建立 腳本或腳本 URL 的 API 是要追蹤的注入點。
留言和搜尋提示是文字功能,我會把它們渲染成 React 文字子節點或賦值給 textContent,讓 瀏覽器完全不把字元解讀成標記。公告契約不同,它允許有限 HTML。我會為段落、強調、清單與 HTTPS 連結定義小型允許清單,讓最終值經過持續維護、基於解析器的淨化器,並只保留一個受審 的原始 HTML 元件。淨化器同時移除事件屬性、樣式、SVG、內嵌頁面與危險 URL 協定,淨化後也 不再串接標記。
惡意留言屬於儲存型傳播,搜尋參數路徑屬於 DOM 型。如果伺服器把請求值立即寫入 HTML 回應, 則屬於反射型。這些標籤幫助判斷暴露方式,具體修正仍依輸出脈絡和注入點選擇。
縱深防禦方面,我會先用 report-only 上線基於 nonce 或 hash 的 CSP,處理違規後再強制執行。 瀏覽器支援時,要求腳本注入點使用 Trusted Types,只允許呼叫核准淨化器的命名策略。我不會 把 CSP、HttpOnly Cookie 或直接回傳原值的 Trusted Types 策略當成主要修正。
驗證會涵蓋儲存資料、URL 資料、畸形標記、事件屬性、SVG 與腳本 URL。我會斷言文字路徑不建立 元素、允許格式能保留、禁止內容被移除、Trusted Types 下原始字串賦值失敗,並確認強制 CSP 能擋住刻意加入的測試注入點。最後持續更新淨化器,把新增注入點納入程式碼審查與靜態檢查。」
常見錯誤
- 所有值使用同一種跳脫 → 瀏覽器解析 HTML、屬性、URL、CSS 和 JavaScript 的規則不同 →
避免把資料放入可執行脈絡,並依精確注入點選擇防禦。
- 刪除腳本標籤後仍用
innerHTML顯示純文字 → 事件屬性、畸形標記、SVG、URL 協定與解析器
行為仍可能被利用 → 改用 JSX 文字或 textContent。
- 對富文字做實體編碼 → 標記會原樣顯示,功能被破壞 → **HTML 是明確產品需求時,使用持續
維護且允許清單收斂的淨化器。**
- 信任只有管理員可寫的內容 → 帳號遭入侵、匯入、遷移與 API 缺陷都可能寫入惡意標記 →
在渲染邊界繼續按不可信內容處理。
- 淨化後繼續串接 HTML → 後續修改可能重新建立可執行結構 → **讓淨化成為進入受審注入點前
的最後轉換。**
- 只用 HTML 編碼驗證
href→ 正確編碼後的值仍可能使用不允許的協定 → **解析 URL,只允許
功能需要的協定和目的地。**
- 認為 React 自動跳脫涵蓋所有路徑 → 逃生口和直接 DOM API 會繞過一般插值 →
盤點每條原始 HTML 與可執行字串路徑。
- 只依賴 CSP → 弱策略、核准腳本或未支援功能都可能留下利用空間 →
先移除危險注入點,再把 CSP 當作額外屏障。
- Trusted Types 策略直接回傳輸入 → 瀏覽器得到可信物件,但轉換過程不可信 →
限制策略名稱,並把建立過程委託給受審淨化器。
- 只確認一個 alert 案例失效 → 單一案例涵蓋不了 URL 協定、畸形標記、替代元素與允許格式
迴歸 → 維護多樣惡意案例和正常富文字案例。
追問及應對
追問一:舊系統有幾百處 innerHTML,如何遷移?
先盤點注入點,依可到達的不可信資料來源、使用者暴露面與權限排序。優先替換所有純文字路徑, 為確實需要 HTML 的少量功能建立一個受審邊界。以 report-only 或診斷模式部署 CSP 與 Trusted Types,找出靜態搜尋遺漏的執行期路徑。暫時的預設 Trusted Types 策略可以記錄舊呼叫,但不能 靜默核准原始字串;呼叫端遷移到明確策略後刪除相容路徑。
追問二:公告必須支援圖片和內嵌影片,方案如何改變?
信任契約會擴大。需要定義圖片來源、URL 協定、尺寸、載入方式與隱私規則;圖片代理可以減少 使用者直接連到第三方。內嵌影片應限制為少量供應商,並使用只開放必要能力的 sandbox。淨化 策略、CSP 指令、使用者同意流程與測試集都要同步變更,任意內嵌頁面、樣式與供應商 HTML 不能直接進入現有公告渲染器。
追問三:嚴格 CSP 讓分析腳本和標籤管理器失效,怎麼辦?
先在 report-only 中執行策略,依業務負責人和腳本用途分類每條違規。刪除不用的腳本,把行內 片段改成核准外部模組,必要腳本依情境使用每回應 nonce 或穩定 hash。使用寬泛萬用字元或 unsafe-inline 會犧牲大量安全邊界。若標籤管理器可以注入任意腳本,應明確把它視為高權限 程式碼路徑,並嚴格限制發布權限。
追問四:伺服器已經淨化公告,為什麼還需要前端邊界?
原始 HTML 元件無論在哪裡執行轉換,都需要可驗證的輸入契約。備用 API、舊資料、匯入、快取、 遷移或策略變更都可能繞過寫入時假設。封裝注入點,只允許目前淨化器輸出進入,並測試該契約; 重複使用已淨化內容時記錄策略版本。伺服器檢查減少壞資料進入系統,渲染邊界防止未驗證值 進入解析器。
追問五:如何調查正式環境中的 XSS 回報?
保存回報 URL、內容 ID、瀏覽器、CSP 報告與對應部署版本,避免在一般帳號中執行載荷。在隔離 環境重現,追蹤精確的資料來源到注入點路徑,並判斷資料來自儲存、請求或第三方。先移除或關閉 脆弱渲染路徑,依需要清除惡意儲存內容、輪替可能暴露的憑證,並檢查暴露時間內的敏感操作。 恢復功能前,把這類載荷加入迴歸案例集。