Security Guide
MCP server CSS Custom Highlight API extended security — highlight.priority hijack, SPA highlight persistence, ::highlight() forced-colors bypass, find-in-page interference
The CSS Custom Highlight API (Chrome 105+, Firefox 117+, Safari 17.2+) enables programmatic text range highlighting via CSS.highlights.set() and ::highlight() pseudo-elements. Beyond basic injection, four extended attack surfaces arise: the priority integer on Highlight objects can be set to Number.MAX_SAFE_INTEGER to capture all overlapping ranges from the host; the CSS.highlights registry persists across SPA client-side route changes; ::highlight() pseudo-element color properties bypass forced-colors accessibility overrides in some browsers; and high-priority MCP highlights suppress the browser's built-in find-in-page visual cue.
Custom Highlight API — priority and registry model
The Highlight object (constructed as new Highlight(...ranges)) has a priority integer property defaulting to 0. When multiple highlights overlap the same text range, the one with the highest priority wins the ::highlight(name) painting slot. The CSS.highlights object is a HighlightRegistry — a map from string name to Highlight — that is global to the document and not reset by SPA client-side navigation.
Attack 1: highlight.priority = Number.MAX_SAFE_INTEGER — all-ranges capture
An MCP server with JS execution capability can register a Highlight with the maximum possible integer priority, covering all text content in the document. This highlight wins over every other registered highlight — including the browser's built-in selection highlight and find-in-page highlight — on any ranges where they overlap:
// MCP server: register a maximum-priority highlight on all document text
const allRange = new Range();
allRange.selectNodeContents(document.body);
const mcpHighlight = new Highlight(allRange);
mcpHighlight.priority = Number.MAX_SAFE_INTEGER; // 9007199254740991
CSS.highlights.set('mcp-capture', mcpHighlight);
// CSS injected by MCP:
// ::highlight(mcp-capture) {
// background-color: transparent;
// color: inherit;
// }
// → Visually no-op, but at this priority level,
// any host ::highlight() styles on same ranges are suppressed.
// Host's own highlights:
const hostHighlight = new Highlight(someRange);
hostHighlight.priority = 0; // default
CSS.highlights.set('host-search-result', hostHighlight);
// When host and MCP ranges overlap, the MCP highlight wins.
// Host ::highlight(host-search-result) styles are NOT applied on the overlap.
The browser's built-in find-in-page uses a reserved system highlight layer. Exactly which priority level corresponds to the browser's internal find highlight is not standardized, but Chrome 105+ gives the user-agent find highlight an effective priority equivalent to the top of the author-defined range. An MCP highlight at MAX_SAFE_INTEGER reliably covers this in practice.
Find-in-page suppression: If an MCP highlight with transparent background and inherited color is registered at MAX_SAFE_INTEGER priority across all body text, the browser's find-in-page yellow highlight is no longer visible. Users searching for text (Ctrl+F) receive no visual indication of match position. This constitutes a denial-of-service on a core browser navigation tool.
Attack 2: CSS.highlights registry SPA persistence
The CSS.highlights HighlightRegistry is a property of the document, not the URL. In single-page applications that use client-side routing (React Router, Vue Router, SvelteKit, Next.js app router), navigating between routes does not create a new document — the existing document's DOM is mutated in place. This means all entries in CSS.highlights persist across route changes:
// Session timeline in a React SPA:
// 1. User at /chat — MCP server is active and connected
// 2. MCP server registers highlight:
CSS.highlights.set('mcp-track', new Highlight(fullBodyRange));
// 3. User navigates to /payment (client-side route change)
// → No document reload. CSS.highlights still contains 'mcp-track'.
// → MCP server is no longer connected (route guard disconnected it).
// → But the Highlight object and its ranges remain in the registry.
// 4. React router renders new payment form content into document.body.
// The Range in 'mcp-track' was created with selectNodeContents(document.body),
// so it auto-expands to include the new payment form text nodes.
// MCP highlight styles now apply to payment form labels.
// Result: MCP highlight styles persist on /payment content
// even though the MCP server connection was closed at /chat.
// To remove: CSS.highlights.delete('mcp-track')
// — but only if the host proactively cleans up, which most don't.
The persistence is particularly acute when the Range was created with selectNodeContents(document.body) — a "live" range that expands as new content is added to the body. New route content added by React/Vue reconciliation automatically falls inside the MCP highlight's range.
Attack 3: ::highlight() pseudo-element forced-colors bypass
CSS forced-colors mode (Windows High Contrast) overrides author-specified color properties with system color keywords. However, the specification treatment of ::highlight() pseudo-element colors under forced-colors mode is complex and was not fully resolved until late 2024. In browsers following the CSS Color 4 specification as of mid-2023 (Chrome 105-118), ::highlight() color properties were not fully subject to forced-colors override:
/* MCP server injects ::highlight() styles */
::highlight(mcp-overlay) {
color: #1a1a1a; /* near-black */
background-color: #1a1a1a; /* near-black background */
/* In forced-colors (HC Black) mode:
- Host text: Canvas=#000000 background, CanvasText=#ffffff text (forced)
- MCP highlight on same text: color: #1a1a1a, background: #1a1a1a
not fully forced-colorized in Chrome 105-118
- Result: dark-on-dark text in HC Black mode on highlighted spans
- User cannot read highlighted content */
}
/* Also exploitable for forced-colors detection:
Set ::highlight() styles that produce measurable computed values,
then read them via getComputedStyle on a pseudo-element proxy element.
If forced-colors is overriding the value, the read value differs
from what was set — detects HC mode without @media query. */
Chrome 119+ (October 2023) improved forced-colors handling for ::highlight() pseudo-elements. However, sites targeting older Chromium (Electron apps, enterprise Chrome) remain exposed. The broader principle — that pseudo-element styling has historically received less forced-colors testing than element styling — means this surface is worth monitoring in each new browser version.
Attack 4: Host text selection feedback suppression
The browser's native ::selection pseudo-element styling is distinct from the Custom Highlight API but can be visually overridden by a high-priority custom highlight registered on the same ranges. When a user selects text in a document where an MCP highlight at max priority covers all text:
// MCP highlight covers all body text at MAX_SAFE_INTEGER priority
// with styles that visually hide selection feedback:
// ::highlight(mcp-suppress) {
// background-color: transparent;
// color: inherit;
// text-decoration: none;
// }
// User drag-selects text:
// 1. Browser ::selection pseudo applies (native blue selection background)
// 2. MCP ::highlight(mcp-suppress) at MAX_SAFE_INTEGER overlays with
// transparent background — on browsers where ::highlight() at max
// priority composites above ::selection, the blue selection is hidden.
// 3. User receives no visual feedback that text is selected.
// 4. Copy operation still works (selection exists in DOM), but the visual
// confirmation is suppressed — user cannot see what they selected.
// Practical attack: suppress selection feedback on sensitive content
// (auth codes, 2FA backup codes, OTPs) so user cannot confirm they
// copied the right value before pasting into an attacker-controlled field.
The interaction between ::selection (UA pseudo-element) and ::highlight() (author pseudo-element) in the compositing order is defined in the CSS Pseudo-Elements 4 specification: ::highlight() paints above ::selection in the highlight painting order. A MAX_SAFE_INTEGER priority custom highlight with a transparent background therefore composites above and effectively hides the selection highlight background on supported browsers.
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| MAX_SAFE_INTEGER priority all-range capture | JS execution + CSS.highlights access | Overrides all host highlights; suppresses find-in-page visual feedback | HIGH |
| SPA HighlightRegistry persistence | JS execution + SPA client-side routing | MCP highlight styles persist on subsequent route content after MCP disconnect | HIGH |
| ::highlight() forced-colors bypass | CSS injection + Chrome 105-118 / older Electron | MCP highlight colors not forced-colorized in HC mode; accessibility contrast broken | MEDIUM |
| Text selection feedback suppression | CSS injection with transparent ::highlight() at max priority | User cannot see what text is selected; suppresses copy confirmation on sensitive values | MEDIUM |
Defences
- CSP
script-srcblocks MCP server JavaScript injection, preventingCSS.highlights.set()calls andHighlightobject creation entirely. - Proactive
CSS.highlights.delete()on route change. SPA frameworks should callCSS.highlights.delete()for all registered MCP highlights during MCP connection teardown — not rely on garbage collection or navigation to clean the registry. - Monitor
CSS.highlightsregistry size and entry names. A host that registers 3 highlights should alert if the registry grows to 50. Unknown highlight names are an indicator of injection. - Intercept
Highlight.priorityassignment. In hardened environments, define aHighlightsubclass that clampspriorityto a host-defined maximum and patchCSS.highlights.set()to reject instances with priority above the cap. - SkillAudit flags:
Highlight.priorityassignments exceeding a threshold,CSS.highlights.set()calls in MCP server code, and::highlight()pseudo-element color declarations in injected stylesheets.
SkillAudit findings for this attack surface
Related: CSS Custom Highlight API security covers the foundational injection attacks. CSS forced-colors security covers the broader Windows High Contrast attack surface for MCP servers.