title: Diagram Theme Config Used to Inject Page-Wide CSS (scope escape, overlay, selector exfil)
id: ATR-2026-02602
rule_version: 2
status: experimental
description: |
  Detects CVE-2026-41149: a mermaid initialisation directive embedded in agent-rendered content (`%%{init: {...}}%%`, or a mermaid.initialize call) whose theme value carries CSS that ESCAPES the diagram's scope. Mermaid runs themeCSS through stylis, which prefixes every selector with `#mermaid-<id>`; a selector written `:not(&)` breaks out of that prefix and applies to the whole document. From there the payload is a full-window phishing overlay or character-at-a-time exfiltration through an attribute-prefix selector whose background url() fires one request per guessed character.
  THE PRECISION LINE, AND WHERE IT WAS DRAWN. `themeCSS` legitimately contains CSS -- `themeCSS: '.node rect { fill: #222 }'` is the documented use, braces and all -- so "contains CSS syntax" is not a signal, and an earlier draft of this rule that fired on a brace pair inside the value was rejected on exactly that benign twin before it was written down.
  TWO FURTHER MARKERS WERE REMOVED IN ADVERSARIAL REVIEW (rule_version 2), each on measured benign fire, and both removals are corrections of the threat model rather than concessions:
  1. `position:fixed` inside a diagram theme was claimed as a viewport-pinned
     overlay. It is not one. Without the stylis escape, stylis prefixes the
     selector with `#mermaid-<id>`, so the declaration is confined to the
     diagram and covers nothing. What the marker actually selected was the
     ordinary fullscreen/lightbox/zoom/print-watermark/pinned-legend feature that
     docs sites build on purpose. Eleven independent benign inputs fired on it:
     a VitePress fullscreen handler, an mkdocs-material manifest, a CHANGELOG
     line, a vitest fixture, a minified bundle line, a Traditional-Chinese blog
     post, a Stack Overflow answer explaining that this does NOT work, and
     several cross-key constructions where the `position:fixed` was in a
     neighbouring key entirely, because `[^\n]{0,200}` runs straight through the
     closing quote of the themeCSS value. The overlay payload is still detected
     when it is an attack, because an attack must carry the scope escape and the
     escape is claimed. Only the escape is load-bearing.

  2. The attribute-prefix selector was matched on ANY attribute name. Only
     `[value^=` (and `$=` / `*=` on the same attribute) is claimed now, because
     `value` is the one attribute that carries a secret the page did not choose
     to publish, and matching on a value PREFIX is something no designer does --
     the selector sees the server-rendered attribute, not what the user typed, so
     it is useless for styling and useful only for guessing. Every other
     attribute name produced benign fire and was removed: `[class^="icon-"]`
     (icon sprites), `[id^="mermaid-"]` (scoping to mermaid's own generated
     container), `[data-testid^="node-"]` (generated themes), `[name^="card"]`
     and `[placeholder^=` (form-field icons), `[title^=` (step badges) -- all of
     them next to a CDN `url()`, all of them leaking nothing. `[href^=` and
     `[src^=` are excluded for the same reason: `a[href^="https"]` beside a
     remote icon url() is a textbook stylesheet. Nine benign inputs fired on the
     untyped version, including the author's own DELIBERATELY-UNCLAIMED
     Google-Fonts `@import` the moment ANY ordinary prefix selector appeared
     within 120 characters of it.

  3. Condition 0's window is `[^\n%]`, not `[^\n]`, so it cannot run out of the
     `%%{...}%%` directive and pick a marker out of the prose that follows it.
     A mitigation note that shows a plain `%%{init}%%` line and then discusses
     the escape in the next sentence was firing before this. MEASURED COST, and
     it is the only one: a payload that puts a literal `%` (e.g. `width:100%`)
     inside a fontFamily/altFontFamily value BEFORE the marker is now missed by
     condition 0. The same payload inside `themeCSS` is still caught, by
     condition 1, which carries no `%` bound -- verified on both, one fires and
     one does not.

  The remaining scope escape additionally requires a declaration block after it (`:not(&) ... {`), because an escape with no rule body changes nothing; that drops prose which merely names the marker after a `themeCSS:` colon.
  A webfont `@import url('https://fonts.googleapis.com/...')` inside themeCSS is a plausible legitimate use of the same mechanism and is DELIBERATELY NOT CLAIMED -- it is a measured true negative, not an oversight.
  Coverage check, re-run in review and CORRECTED. The only rule that mentions mermaid, ATR-2026-01967, is scoped to DeepChat IPC/MCP registration -- that part of the author's claim holds. What the author missed is a sibling from this same sweep: ATR-2026-02704 detects the CSS attribute-selector exfiltration primitive generically, and it fires on three of the four exfil payloads here. It reached the same conclusion independently -- it only claims SECRET-bearing attributes (`value`, `token`, `csrf`, `secret`, `password`, `api_key`, `otp`) -- which is corroboration that the untyped attribute match this rule shipped with was the wrong line. The exfil branch here is therefore largely redundant with 02704 and is kept only for the themeCSS-scoped ordering 02704 does not cover (url() before the selector). The `:not(&)` stylis scope escape is this rule's own, and nothing else detects it.
author: ATR Community
date: 2026/08/23
schema_version: '0.1'
detection_tier: pattern
maturity: test
severity: high
references:
  owasp_llm:
    - LLM02:2025
  owasp_agentic:
    - ASI04:2026
  mitre_atlas:
    - AML.T0051 - LLM Prompt Injection
    - AML.T0051.001 - Indirect
  cve:
    - CVE-2026-41149
compliance:
  owasp_agentic:
    - id: ASI04:2026
      context: Content the agent renders escapes the boundary it was given and takes over the surrounding interface, which is an agent output-handling failure rather than a model failure.
      strength: primary
  owasp_llm:
    - id: LLM02:2025
      context: 'Insecure output handling: markup produced or relayed by the model reaches a renderer that grants it document-wide effect.'
      strength: primary
  eu_ai_act:
    - article: '15'
      context: Article 15 requires resilience against attempts to manipulate system output; a diagram config that repaints the whole window is exactly such a manipulation, and detection is the control.
      strength: primary
    - article: '14'
      context: A full-window overlay defeats human oversight by design -- the operator sees the attacker's surface instead of the system's -- so detecting it is an Article 14 control.
      strength: secondary
    - article: '9'
      context: Rendered-output injection is a documented risk class for agent interfaces; these events are the Article 9 monitoring evidence.
      strength: secondary
  nist_ai_rmf:
    - function: Manage
      subcategory: MG.2.3
      context: Runtime treatment for untrusted content reaching a rendering surface with more authority than intended.
      strength: primary
    - function: Map
      subcategory: MP.5.1
      context: Catalogues CSS-only injection as a distinct risk from script injection, which existing rules already cover.
      strength: secondary
    - function: Measure
      subcategory: MS.2.7
      context: Detection events document the security of the output-rendering path as MEASURE 2.7 requires.
      strength: secondary
  iso_42001:
    - clause: '8.1'
      context: Clause 8.1 operational control over the rendering pipeline that turns model output into a user-facing surface.
      strength: primary
    - clause: '6.2'
      context: Preventing interface takeover and selector-based data exfiltration is an AIMS information security objective under clause 6.2.
      strength: secondary
    - clause: '8.4'
      context: Impact assessment under 8.4 must account for what a rendered artifact can do to the viewer; these events are the evidence for that path.
      strength: secondary
tags:
  category: context-exfiltration
  subcategory: rendered-output-injection
  scan_target: both
  confidence: medium
agent_source:
  type: llm_io
  framework:
    - any
  provider:
    - any
detection:
  conditions:
    - field: content
      operator: regex
      value: '%%\s*\{[^\n]{0,240}?\b(?:themeCSS|altFontFamily|fontFamily)\b["'']?\s*:\s*["''][^\n%]{0,200}?(?::not\s*\(\s*&[^\n{]{0,20}?\)[^\n]{0,40}?\{|\[\s*value\s*[\^$*]=[^\n]{0,60}?\{[^\n]{0,90}?url\s*\(\s*["'']?(?:https?:)?//|url\s*\(\s*["'']?(?:https?:)?//[^\n]{0,120}?\[\s*value\s*[\^$*]=)'
      description: Mermaid init directive whose theme value breaks the stylis scope with :not(&) and opens a rule body, or pairs a SECRET-BEARING attribute-prefix selector with a remote url()
    - field: content
      operator: regex
      value: \bthemeCSS\b["']?\s*:\s*["'][^\n]{0,200}?(?::not\s*\(\s*&[^\n{]{0,20}?\)[^\n]{0,40}?\{|\[\s*value\s*[\^$*]=[^\n]{0,60}?\{[^\n]{0,90}?url\s*\(\s*["']?(?:https?:)?//|url\s*\(\s*["']?(?:https?:)?//[^\n]{0,120}?\[\s*value\s*[\^$*]=)
      description: A themeCSS value carrying the same two escape markers, in the JS/JSON config form
  condition: any
  false_positives:
    - 'THE ONE REAL RESIDUAL, measured, and it is a whole class rather than one shape: ANY prose that reproduces the payload -- the theme key, a colon, a quote, then the stylis escape opening a rule body -- is textually identical to the attack, so no pattern separates them. Three independent constructions fire and are accepted: a CVE advisory quoting the payload, a CHANGELOG line announcing the fix, and a GitHub bug report using the escape as its reproduction case. It does not occur in 12,830 benign corpus samples or in 556 repo doc/skill files. This rule''s own prose deliberately never reassembles that sequence, and that is verified: it does not fire on itself minus its test_cases'
    - A theme that loads a remote webfont AND matches on a value PREFIX (`[value^=`) in the same 120 characters for unrelated reasons -- constructible, never observed, and there is no styling reason to write it
    - 'NOT a false positive any more, and recorded so it is not re-added: a mermaid theme that uses position:fixed for a fullscreen, lightbox, zoom, print-watermark or pinned-legend feature. Confined by stylis, harmless, and the marker was removed'
    - 'NOT a false positive any more, and recorded so it is not re-widened: [class^=], [id^=], [data-*^=], [name^=], [placeholder^=], [title^=] or [href^=] beside a remote url(). Those attributes carry no secret, so the selector-exfil primitive does not apply and the marker no longer matches them'
response:
  actions:
    - alert
    - snapshot
  auto_response_threshold: high
  message_template: |
    [ATR-2026-02602] A diagram theme configuration carries CSS that leaves the diagram's scope -- a stylis `:not(&)` escape opening a rule body, or a secret-bearing attribute-prefix selector paired with a remote url(). Rendered content can repaint the whole window or leak field values one character at a time. Do not render until the source of this block is established.
confidence: 70
