Security reference · CSS injection · Pseudo-class attacks · Same-origin link selector

MCP server CSS :local-link pseudo-class security

The CSS :local-link pseudo-class (CSS Selectors 4) selects anchor elements whose href points to the same origin as the current page. Navigation links, internal section anchors, and "back to docs" links all qualify. Because most real install pages contain at least one same-origin anchor — navigation to terms, docs, or a related page on the same domain — :local-link provides a near-universal zero-injection trigger for consent hiding that fires purely from the host page's own link structure.

CSS :local-link attack surface in MCP consent UIs

CSS patternWhy it firesEvasion quality
a:local-link ~ .consent-disclosure { display:none }Any link to the same origin as current page qualifies; most navigation links on install pages are same-originZero HTML injection; fires on host page's own navigation anchors
:local-link(0) ~ .consent { display:none }:local-link(0) matches links to the exact current URL/path; MCP can craft a link to the current pageMore specific selector; MCP controls whether a same-path link is injected
a:local-link + .consent-text { display:none }Adjacent sibling combinator; fires when "read our terms" or "back to docs" link immediately precedes consent textMany install pages place policy links directly before consent text — zero injection needed
Browser support scoping:local-link is a CSS Selectors 4 draft with limited implementation; attack is scoped to implementing browsersLimited but growing; attacks are real in Chrome/Firefox builds with the feature flag enabled

Attack 1: a:local-link fires on same-origin navigation links

The :local-link pseudo-class matches any anchor element whose href attribute has the same origin as the document — the same protocol, domain, and port. On any real install page that has navigation to /terms, /docs, /privacy, or any other path on the same domain, the navigation anchors are :local-link targets from page load:

/* Malicious CSS — SA-CSS-LOCAL-001 */
/* Fires on any anchor pointing to the same origin as the current page */
a:local-link ~ .consent-disclosure {
  display: none;
}

/* Why same-origin links are universal on install pages:
   A typical MCP install page at https://example-mcp.com/install/ will have:
   - "Read our terms" href="/terms"              — same origin ✓
   - "Back to docs" href="/docs/installation"    — same origin ✓
   - Logo href="/"                               — same origin ✓
   - "Privacy policy" href="/privacy"            — same origin ✓

   Any of these anchors appearing before .consent-disclosure in DOM order
   triggers the rule. The MCP server does not need to inject any HTML —
   the host page's own navigation provides the :local-link trigger.

/* The sibling combinator (~) matches any following sibling, not just adjacent ones.
   As long as at least one a:local-link appears anywhere before .consent-disclosure
   in the same parent's children, the rule fires.

function detectLocalLinkConsentHiding() {
  const findings = [];
  // Check if :local-link is supported
  try { document.querySelector(':local-link'); } catch(_) { return findings; }

  const localLinks = document.querySelectorAll('a:local-link');
  if (localLinks.length === 0) return findings;

  for (const link of localLinks) {
    let sib = link.nextElementSibling;
    while (sib) {
      if (sib.matches('[class*="consent"], .terms-section, .consent-disclosure') &&
          getComputedStyle(sib).display === 'none') {
        findings.push({ id: 'SA-CSS-LOCAL-001', severity: 'high',
          message: `a:local-link "${link.textContent.trim()}" (href=${link.href}) precedes hidden consent sibling` });
      }
      sib = sib.nextElementSibling;
    }
  }
  return findings;
}

Zero-injection vector: SA-CSS-LOCAL-001 requires no HTML injection — only CSS injection. The host page's own navigation links are the trigger. An HTML-injection scanner finds nothing because nothing was injected into the HTML.

Attack 2: :local-link(0) — exact path match

The functional notation :local-link(N) (proposed in Selectors 4) accepts a depth argument: 0 means the link points to exactly the same URL as the current page. An MCP server can inject a single anchor pointing to the current URL — a "refresh" link, a "share this page" link, or a canonical self-reference — to guarantee a :local-link(0) match:

/* Malicious CSS — SA-CSS-LOCAL-002 */
/* :local-link(0) matches links to the exact current URL */
:local-link(0) ~ .consent {
  display: none;
}

/* Malicious HTML injection — single anchor pointing to current URL */
<a href="" aria-hidden="true" style="display:none">current page</a>
<div class="consent">This server requests permissions...</div>

/* A link with href="" is equivalent to a link to the current URL.
   It satisfies :local-link(0) (same origin and same exact path).
   The element can be hidden (display:none) — :local-link still applies
   to hidden anchor elements, because the selector matches the element,
   not its visibility.

/* This is a minimal injection attack: one hidden anchor element, no visible
   change to the page, no suspicious class or attribute. The href="" pattern
   is used in many legitimate contexts (anchor elements for JS interception,
   breadcrumb current-page markers, etc.) */

Attack 3: a:local-link + .consent-text adjacent sibling combinator

Many install pages place a "read our privacy policy" or "view terms" link directly before the consent checkbox or disclosure text. The adjacent combinator + requires strict DOM adjacency — no elements between the trigger and the target. When the page structure puts a policy link immediately before the consent text, a:local-link + .consent-text { display:none } fires with zero injection:

/* Malicious CSS — SA-CSS-LOCAL-003 */
/* Fires when a local-link anchor is immediately before consent text */
a:local-link + .consent-text {
  display: none;
}

/* Common host-page HTML structure that creates this adjacency: */
<!-- Typical install page structure — NO malicious injection needed -->
<a href="/privacy">Read our privacy policy</a>   <!-- :local-link ✓ -->
<p class="consent-text">                          <!-- direct sibling of above -->
  By clicking Install, you agree to the processing of your data...
</p>

/* The "Read our privacy policy" link satisfies :local-link (same origin).
   The consent text paragraph is its immediate next sibling.
   The CSS rule hides the consent text — but only on the adjacent pattern.
   On pages where other elements appear between the policy link and the
   consent text, this specific attack does not fire.
   MCP selects this pattern opportunistically, counting on common
   install-page structural conventions. */

/* Detection: check adjacency of local-link anchors and consent elements */
function detectAdjacentLocalLinkConsent() {
  const findings = [];
  try { document.querySelector(':local-link'); } catch(_) { return findings; }

  for (const link of document.querySelectorAll('a:local-link')) {
    const nextEl = link.nextElementSibling;
    if (nextEl?.matches('[class*="consent"], .terms-section') &&
        getComputedStyle(nextEl).display === 'none') {
      findings.push({ id: 'SA-CSS-LOCAL-003', severity: 'high',
        message: `a:local-link "${link.href}" is adjacent to hidden consent element — SA-CSS-LOCAL-003` });
    }
  }
  return findings;
}

Attack 4: Browser support scoping and the growing :local-link attack window

As of 2026, :local-link is a CSS Selectors 4 Level 4 feature still in draft status, with limited browser implementation. The attack surface is currently scoped to browsers that implement it:

/* SA-CSS-LOCAL-004: Browser support analysis */

/* Current implementation status (2026):
   Chrome/Chromium: experimental flag (--enable-experimental-web-platform-features)
   Firefox: not yet implemented in stable; available in Nightly with dom.css.selectors4 pref
   Safari: not yet implemented

   Attack surface today:
   - Users on Chrome with experimental features enabled
   - Firefox Nightly users
   - Chromium-based browsers that have enabled the flag

   Attack surface trajectory:
   - CSS Selectors 4 is progressing toward CR (Candidate Recommendation)
   - :local-link was originally proposed in Selectors 3 and deferred
   - Growing pressure from web authors for same-origin detection without JS
   - Expected stable implementation in Chrome/Firefox within 1-2 years

/* Graceful degradation in browsers without support:
   The CSS rule is silently ignored — consent is visible.
   This means the attack is opportunistic: it harms users on
   implementing browsers without being detected on non-implementing ones.
   MCP servers that include SA-CSS-LOCAL-001 in their stylesheet
   accept that the attack works on some users but not others.

/* SkillAudit detection approach for non-supporting browsers:
   CSS rule text analysis (not computed style evaluation):
   Scan all stylesheet rule selectorText values for ':local-link'
   regardless of browser support — the presence of the rule in the
   injected stylesheet is itself a finding, regardless of activation. */

function detectLocalLinkRulesInStylesheets() {
  const findings = [];
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        if (!(rule instanceof CSSStyleRule)) continue;
        if (rule.selectorText.includes(':local-link') &&
            /display\s*:\s*none|visibility\s*:\s*hidden/.test(rule.style.cssText)) {
          findings.push({ id: 'SA-CSS-LOCAL-004', severity: 'medium',
            message: `CSS rule uses :local-link to hide element: ${rule.selectorText} — browser-dependent attack surface` });
        }
      }
    } catch (_) {}
  }
  return findings;
}

Forward-looking risk: SA-CSS-LOCAL-001 through -003 represent an emerging attack class. As :local-link achieves stable browser support, the attack window grows from "experimental browser users" to "every user." MCP servers that include :local-link consent-hiding rules in their CSS today are pre-positioned for the broader attack window that stable support will open.

SkillAudit findings for CSS :local-link consent attacks

HighSA-CSS-LOCAL-001 — a:local-link ~ .consent-disclosure { display:none }. Same-origin navigation links trigger :local-link on every real install page; zero HTML injection; fires on host page's own navigation structure. Requires browser implementing :local-link.
HighSA-CSS-LOCAL-002 — :local-link(0) ~ .consent { display:none }. Exact-path match; MCP injects minimal hidden anchor with href="" pointing to current URL. Anchor can be display:none — :local-link still fires. Minimal injection footprint.
HighSA-CSS-LOCAL-003 — a:local-link + .consent-text { display:none }. Adjacent sibling combinator; exploits common "read our policy" link directly before consent text DOM structure. Zero injection on pages with this natural layout.
MediumSA-CSS-LOCAL-004 — :local-link rules detected in stylesheet regardless of browser activation. Pre-positioned attack; rules present in CSS even if not active in current browser. Static rule scan flags forward-looking risk.

Detection

/**
 * SkillAudit: CSS :local-link consent attack detection
 * Covers SA-CSS-LOCAL-001 through -004
 */
function auditLocalLinkConsentAttacks() {
  const results = [];

  // Static: scan all stylesheets for :local-link rules (SA-CSS-LOCAL-004)
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        if (!(rule instanceof CSSStyleRule)) continue;
        if (rule.selectorText.includes(':local-link')) {
          results.push({ id: 'SA-CSS-LOCAL-004', severity: 'medium',
            message: `:local-link rule found: ${rule.selectorText}` });
        }
      }
    } catch (_) {}
  }

  // Dynamic: check :local-link selectors if browser supports them
  let supportsLocalLink = false;
  try { document.querySelector(':local-link'); supportsLocalLink = true; } catch(_) {}

  if (supportsLocalLink) {
    for (const link of document.querySelectorAll('a:local-link')) {
      let sib = link.nextElementSibling;
      while (sib) {
        if (sib.matches('[class*="consent"], .terms-section') &&
            getComputedStyle(sib).display === 'none') {
          results.push({ id: 'SA-CSS-LOCAL-001', severity: 'high',
            message: `a:local-link precedes hidden consent element` });
        }
        if (sib === link.nextElementSibling &&
            sib.matches('[class*="consent"]') &&
            getComputedStyle(sib).display === 'none') {
          results.push({ id: 'SA-CSS-LOCAL-003', severity: 'high',
            message: `a:local-link is adjacent to (directly before) hidden consent element` });
        }
        sib = sib.nextElementSibling;
      }
    }
  }

  return results;
}

Related MCP consent attack research

Audit your MCP server for :local-link consent-hiding attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-LOCAL findings.