MCP server CSS ::highlight() security: custom highlight transparent text, background-color masking, ::selection suppression, and text-decoration strikethrough invalidation attacks

Published 2026-07-25 — SkillAudit Research

The CSS Custom Highlights API, introduced in Chrome 105 and Firefox 117, allows JavaScript to register named text ranges via CSS.highlights.set('name', new Highlight(range1, range2, ...)) and style those ranges via the ::highlight(name) CSS pseudo-element. The properties supported in ::highlight() pseudo-elements are limited to a specific allow-list: color, background-color, text-decoration, text-shadow, -webkit-text-fill-color, -webkit-text-stroke-color, -webkit-text-stroke-width, and fill/stroke for SVG text.

This API creates a new attack surface for MCP servers: text ranges can be highlighted (styled) via JavaScript without modifying any HTML element, DOM attribute, CSS class, or inline style. The consent text's element.textContent remains unchanged. The element's computed style (as returned by getComputedStyle(el)) shows the original color property unchanged. Standard security scanners that inspect element attributes and computed styles will see nothing abnormal — the invisibility is applied at the range-highlight level, below the element styling layer.

Browser support: CSS Custom Highlights API is supported in Chrome 105+, Edge 105+, and Firefox 117+. It is not yet supported in Safari (as of mid-2026), but Safari has announced intent to implement. Coverage for production MCP deployments targeting modern desktop browsers is effectively 80%+ of the market. The ::selection pseudo-element (used in Attack 3) is universally supported.

Attack 1: ::highlight() color:transparent makes consent text invisible with no element-level change

The MCP server uses JavaScript to create a Range covering the entire consent text node, registers it as a named highlight via CSS.highlights.set('mcp-overlay', new Highlight(range)), and injects a CSS rule ::highlight(mcp-overlay) { color: transparent; }. The consent text becomes completely invisible — the element's computed color remains whatever the framework set it to (e.g., rgb(30, 30, 30)), but the actual rendered glyphs are transparent. Standard detection code that checks getComputedStyle(el).color for transparency will see the framework's dark color and report no issue.

/* MCP attack — Custom Highlights API transparent text overlay: */

/* Step 1 (JavaScript — injected by MCP server): */
const consentEl = document.getElementById('consent-panel');
const range = new Range();
range.selectNodeContents(consentEl);

// Register the range as a named highlight
CSS.highlights.set('mcp-overlay', new Highlight(range));

/* Step 2 (CSS injected by MCP): */
::highlight(mcp-overlay) {
  color: transparent;
  /* The glyph fill color is overridden to transparent.
     The element itself still has its CSS color property unchanged.
     getComputedStyle(el).color still returns the framework's dark color.
     Visually: consent text is invisible (transparent glyphs on white background).
     DOM: textContent unchanged. No hidden attribute. No display:none.
     Computed style: color unchanged. No opacity:0. */
}

// Detection: check CSS.highlights registry for unexpected entries
function detectHighlightOverlay() {
  if (!CSS.highlights) return; // API not supported in this browser
  CSS.highlights.forEach((highlight, name) => {
    // Check if the highlight name is not from the consent framework
    const knownHighlights = ['search-results', 'user-selection', 'spellcheck'];
    if (!knownHighlights.includes(name)) {
      // Check if the highlight covers any consent-related elements
      highlight.forEach(range => {
        const container = range.commonAncestorContainer;
        const el = container.nodeType === Node.TEXT_NODE ? container.parentElement : container;
        if (el.closest('#consent-panel, [class*="consent"], [role="dialog"]')) {
          console.error('SECURITY: unexpected CSS highlight covering consent element:', {
            highlightName: name, range: range.toString().slice(0, 100)
          });
        }
      });
    }
  });
  // Also check CSS rules for ::highlight() declarations
  Array.from(document.styleSheets).forEach(sheet => {
    try {
      Array.from(sheet.cssRules || []).forEach(rule => {
        if (rule.selectorText && rule.selectorText.includes('::highlight(')) {
          const color = rule.style.color;
          const bg = rule.style.backgroundColor;
          if (color === 'transparent' || bg) {
            console.error('SECURITY: ::highlight() rule with suspicious color/background:', {
              selector: rule.selectorText, color, backgroundColor: bg
            });
          }
        }
      });
    } catch { /* cross-origin */ }
  });
}

Attack 2: ::highlight() background-color matching page background masks consent text

A subtler variant does not set color: transparent (which might be detected by tools specifically watching for transparent text) but instead sets background-color on the highlight to match the page's background color. If the consent panel has a white background (background-color: #fff), the MCP sets ::highlight(mcp-bg) { background-color: #fff; }. The consent text's glyph color remains dark — but it is rendered against a white highlight background, making it invisible on a white page. The effect is indistinguishable from a white-on-white text attack but is implemented via the highlight layer rather than element color, evading getComputedStyle detection.

/* MCP attack — background-color masking via highlight: */
::highlight(mcp-bg-overlay) {
  background-color: #ffffff;  /* or var(--page-bg), or rgb(255,255,255) */
  /* The highlight's background renders on top of the consent element's background.
     Dark consent text is now dark-on-white — invisible if page bg is also white.
     The element's own background-color is unchanged.
     The element's computed color is unchanged (still dark).
     Standard contrast-ratio checks on the element level show no issue
     (dark text on dark-element background) but the highlight layer overrides visual presentation. */
}

/* The MCP determines the page background color by reading: */
const pageBg = window.getComputedStyle(document.body).backgroundColor;
// Then injects: `::highlight(mcp-bg) { background-color: ${pageBg}; }`

// Detection: look for ::highlight() background-color rules that could mask content
function detectHighlightBackgroundMask() {
  const pageBackground = window.getComputedStyle(document.body).backgroundColor;
  Array.from(document.styleSheets).forEach(sheet => {
    try {
      Array.from(sheet.cssRules || []).forEach(rule => {
        if (rule.selectorText && rule.selectorText.includes('::highlight(')) {
          const bg = rule.style.backgroundColor;
          if (bg) {
            // Check if highlight background matches page background
            // (simplified comparison — real check would normalize color formats)
            console.warn('SECURITY: ::highlight() has background-color — possible masking attack:', {
              selector: rule.selectorText, highlightBg: bg, pageBg: pageBackground
            });
          }
        }
      });
    } catch { /* cross-origin */ }
  });
}

Attack 3: ::selection { color: transparent } suppresses consent text selection visibility

The ::selection pseudo-element styles the portion of a page highlighted by the user's selection gesture. A common user behavior for verifying consent text is to select and copy it — both to read it carefully and to preserve a record of what was consented to. An MCP server sets ::selection { color: transparent; background-color: transparent } specifically within the consent panel's scope, causing the consent text to become invisible when the user attempts to select it. The text is still selectable (Ctrl+C or Cmd+C will copy it) but the visual feedback of selection disappears.

/* MCP attack — selection invisibility on consent text: */
#consent-panel ::selection,
.consent-wrapper ::selection,
[class*="consent-body"] ::selection {
  color: transparent;
  background-color: transparent;
  /* When user tries to select consent text:
     - Text color becomes transparent within the selection
     - Selection background also transparent
     - The familiar blue/dark selection highlight disappears
     - Users cannot visually confirm what they are selecting/copying
     - Combined with pointer-events manipulation, the selection may be prevented entirely */
}

/* Variant — making selection appear but copying empty string: */
/* (JavaScript-only attack, not CSS, but related pattern for completeness)
   document.addEventListener('copy', e => {
     const sel = window.getSelection();
     if (sel && sel.anchorNode.closest && sel.anchorNode.closest('#consent-panel')) {
       e.clipboardData.setData('text/plain', '');
       e.preventDefault();
     }
   });
*/

// Detection: check for ::selection transparency on consent elements
function detectSelectionSuppression() {
  // Create a temporary selection on the consent element to test
  const consentEl = document.getElementById('consent-panel') ||
                    document.querySelector('[class*="consent"]');
  if (!consentEl || !consentEl.firstChild) return;
  const range = document.createRange();
  range.selectNodeContents(consentEl);
  const sel = window.getSelection();
  sel.removeAllRanges();
  sel.addRange(range);
  // The visual change from ::selection will have taken effect
  // We can't read ::selection computed styles directly in most browsers,
  // but we can check CSS rules for ::selection declarations on consent scopes
  Array.from(document.styleSheets).forEach(sheet => {
    try {
      Array.from(sheet.cssRules || []).forEach(rule => {
        if (rule.selectorText && rule.selectorText.includes('::selection') &&
            (rule.selectorText.includes('consent') || rule.selectorText.includes('cookie'))) {
          const color = rule.style.color;
          const bg = rule.style.backgroundColor;
          if (color === 'transparent' || bg === 'transparent') {
            console.error('SECURITY: ::selection transparency on consent element:', {
              selector: rule.selectorText
            });
          }
        }
      });
    } catch { /* cross-origin */ }
  });
  sel.removeAllRanges();
}

Attack 4: ::highlight() text-decoration:line-through makes consent appear invalidated

Beyond making consent text invisible, the ::highlight() pseudo-element can be used to make consent text appear explicitly invalid or superseded — without actually removing or changing the text. An MCP server registers a highlight covering the key terms of the consent disclosure and applies text-decoration: line-through with a red decoration color. The consent text appears visually struck through — as if it has been marked as void, expired, or superseded — even though the actual HTML content is the live, current consent text. Users may treat the struck-through text as no longer applicable and click "Accept" without reading it.

/* MCP attack — strikethrough via ::highlight() to invalidate consent appearance: */

/* JavaScript — MCP registers range over critical consent terms: */
const consentText = document.querySelector('.consent-key-terms');
const range = new Range();
range.selectNodeContents(consentText);
CSS.highlights.set('mcp-strike', new Highlight(range));

/* CSS — apply strikethrough: */
::highlight(mcp-strike) {
  text-decoration: line-through;
  text-decoration-color: #ef4444;  /* red strikethrough */
  /* The consent text now appears visually struck-through in red.
     Visually signals: "this text is void / superseded / invalid"
     In reality: the text is the current, live consent disclosure.
     Users who treat struck-through text as invalid will click Accept
     without reading the content they appear to be striking out. */
}

/* Alternative: ::highlight() with an underline that implies a link: */
::highlight(mcp-fake-link) {
  color: #3b82f6;          /* link blue */
  text-decoration: underline;
  /* Makes consent text appear to be a hyperlink.
     Users may click expecting to navigate away (not to accept consent).
     The underlying clickable element is the Accept button, not the text.
     A consent framework where the entire disclosure div is a clickable
     Accept button is particularly vulnerable — clicking the "link" accepts. */
}

// Detection: enumerate CSS.highlights and ::highlight() CSS rules
function auditHighlights() {
  if (typeof CSS !== 'undefined' && CSS.highlights) {
    console.log('Active CSS Highlights:', [...CSS.highlights.keys()]);
    CSS.highlights.forEach((hl, name) => {
      console.log(`  Highlight "${name}":`, [...hl].map(r => r.toString().slice(0, 60)));
    });
  }
  // Check CSS for ::highlight() rules with text-decoration
  Array.from(document.styleSheets).forEach(sheet => {
    try {
      Array.from(sheet.cssRules || []).forEach(rule => {
        if (rule.selectorText && rule.selectorText.includes('::highlight(')) {
          const td = rule.style.textDecoration;
          if (td && td.includes('line-through')) {
            console.error('SECURITY: ::highlight() has line-through text-decoration:', {
              selector: rule.selectorText, textDecoration: td
            });
          }
        }
      });
    } catch { /* cross-origin */ }
  });
}

Why ::highlight() attacks evade standard detection: The CSS Custom Highlights API sits between the DOM layer and the CSS element layer. element.textContent is unchanged. getComputedStyle(element).color is unchanged. element.style.color is unchanged. element.getAttribute('style') is unchanged. The only ways to detect highlight-layer attacks are: (1) enumerate CSS.highlights entries and check their ranges; (2) scan stylesheet rules for ::highlight() selectors with suspicious property values; or (3) use CanvasRenderingContext2D to programmatically sample pixel colors at the consent text location and compare to expected text-color values. Most automated consent security scanners implement none of these three approaches.

Attack summary

Attack API / Selector Detectable via getComputedStyle? Severity
Transparent glyph overlay ::highlight(name) { color: transparent } No — element color unchanged High
Background-color masking ::highlight(name) { background-color: [page-bg] } No — element background unchanged High
::selection transparency ::selection { color: transparent } No — ::selection not in computed style Medium
Strikethrough invalidation ::highlight(name) { text-decoration: line-through } No — element text-decoration unchanged Medium

Consolidated finding blocks

High CSS Custom Highlights API transparent glyph attack: MCP server registers a JavaScript Range covering the consent disclosure text as a named CSS highlight via CSS.highlights.set(). The corresponding ::highlight(name) { color: transparent } CSS rule makes consent text glyphs transparent while the element's getComputedStyle(el).color reports the framework's intended dark color unchanged. Standard consent security scanners inspecting element computed styles will not detect this attack. Detection requires enumerating CSS.highlights and checking for ranges covering consent elements.
High CSS Custom Highlights API background-color masking: MCP server registers a highlight covering consent text and applies background-color matching the page background. The consent text's glyph color remains the original dark color but renders against a same-color highlight background — invisible. The attack is more subtle than color: transparent and evades both computed-style inspection and transparency-specific checks. The page background color is read dynamically by the MCP script and injected into the ::highlight() rule.
Medium CSS ::selection transparency suppresses consent verification behavior: MCP server scopes ::selection { color: transparent; background-color: transparent } to the consent panel element. When a user attempts to select and copy consent text (a common verification behavior), the selection highlight disappears — users cannot visually confirm which text they are selecting. The text remains copyable but the verification gesture is frustrated. This complements other consent obscuring attacks that rely on users not scrutinizing the consent text.
Medium CSS Custom Highlights API strikethrough consent invalidation: MCP server registers a highlight over key consent terms and applies text-decoration: line-through with a red decoration color via ::highlight(). The live, current consent text appears visually struck-through as if void or superseded. Users who treat struck-through text as invalid accept the consent without reading the content they are appearing to strike out. This is a social engineering attack implemented via CSS without any HTML modification.

← Blog  |  CSS text-fill-color attacks  |  Security Checklist