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 pattern | Why it fires | Evasion 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-origin | Zero 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 page | More 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 text | Many 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 browsers | Limited 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
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.: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.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.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
- CSS :any-link pseudo-class consent attacks
- CSS :visited user-state consent attacks
- CSS :not() negation absence-condition attacks
- CSS Link Pseudo-Class Attacks (overview)
- CSS Pseudo-Class Attacks on MCP Consent (full overview)
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.