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.

AttackPrerequisiteWhat it enablesSeverity
MAX_SAFE_INTEGER priority all-range captureJS execution + CSS.highlights accessOverrides all host highlights; suppresses find-in-page visual feedbackHIGH
SPA HighlightRegistry persistenceJS execution + SPA client-side routingMCP highlight styles persist on subsequent route content after MCP disconnectHIGH
::highlight() forced-colors bypassCSS injection + Chrome 105-118 / older ElectronMCP highlight colors not forced-colorized in HC mode; accessibility contrast brokenMEDIUM
Text selection feedback suppressionCSS injection with transparent ::highlight() at max priorityUser cannot see what text is selected; suppresses copy confirmation on sensitive valuesMEDIUM

Defences

SkillAudit findings for this attack surface

HIGHMAX_SAFE_INTEGER priority highlight: MCP server registers Highlight with priority Number.MAX_SAFE_INTEGER covering all body text, overriding host highlights and suppressing find-in-page visual cue
HIGHSPA highlight persistence: MCP server registers highlights that survive client-side route changes via selectNodeContents(document.body) range, affecting subsequent route content after MCP connection closed
MEDIUM::highlight() forced-colors bypass: Injected ::highlight() pseudo-element styles not fully forced-colorized on Chrome 105-118; MCP colors may persist in Windows High Contrast mode, breaking accessibility contrast
MEDIUMText selection feedback suppression: MCP highlight at max priority with transparent background composites above ::selection, hiding user text selection visual feedback on sensitive content

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.

← Blog  |  Security Checklist