Security reference · CSS injection · Pseudo-class attacks · Hyperlink presence

MCP server CSS :any-link pseudo-class security

The CSS :any-link pseudo-class (CSS Selectors 4) matches any element that acts as a hyperlink — specifically <a> and <area> elements with an href attribute — regardless of whether the link has been visited. It is the privacy-safe equivalent of :link, :visited combined. For MCP server consent attacks, :any-link creates a near-universal trigger: every real install page has at least one link — a privacy policy link, a documentation link, a "learn more" anchor — meaning the rule fires on any install page with zero injected HTML.

:any-link vs :link/:visited — why the distinction matters for attacks

SelectorWhat it matchesAttack advantage
:any-linkAll <a href> and <area href> elements regardless of visited stateBroadest link selector — fires on any page with any link; unaffected by browser privacy protections that partition :visited
:linkUnvisited links only — browsers may restrict this in privacy mode or partition by siteNarrower; may not match if user has visited the link before; subject to privacy partitioning
:visitedVisited links — heavily restricted by browsers; limited CSS properties allowed (color, background-color)Cannot hide consent via display:none with :visited alone — browser blocks the property application
:-webkit-any-linkWebKit vendor-prefix alias for :any-link (Chrome, Safari) — NOT in FirefoxCreates browser-specific gap: Firefox auditors do not fire the rule; iOS devices (all WebKit) are affected

:any-link is universally supported and broadly applicable: Unlike :local-link (CSS Selectors 4 draft, limited support) or :visited (heavy privacy restrictions), :any-link is stable (Chrome 65+, Firefox 50+, Safari 9+), unrestricted in property application, and fires on every page that has any hyperlink — making it one of the widest zero-injection attack surfaces in the CSS pseudo-class space.

CSS :any-link attack surface in MCP consent UIs

CSS patternWhy it firesEvasion quality
a:any-link ~ .consent-disclosure { display:none }Every anchor with href on any install page fires this from load; zero HTML injectionFires immediately on every real-world install page; no user action required
a:any-link[href*="/install"] ~ .consent { display:none }MCP's own "Install Server" link (href="/install") triggers the rule; natural presence on install pagesAttribute constraint makes the rule look benign; installer link is MCP's own injected anchor
a:any-link:not([data-audit-visible]) ~ .consent-disclosure { display:none }Links without MCP's tracking attribute (which MCP never adds) trigger the absence conditionAbsence-condition: fires on all natural links (none have data-audit-visible); MCP-added links would also fire
area:any-link ~ .consent-section { display:none }MCP injects a hidden <map>/<area href=""> element before the consent section; zero visible outputArea elements are visually invisible; audit DOM scan may not check <area> elements for :any-link matching

Attack 1: a:any-link ~ .consent-disclosure — universal hyperlink presence attack

The general sibling combinator with a:any-link creates a rule that fires the moment any anchor with an href attribute precedes the consent disclosure in the DOM. On any real install page — which invariably contains navigation links, "read our privacy policy" anchors, documentation links, and more — this rule fires from the first paint with zero injected HTML. The only artifact MCP needs to inject is the CSS rule itself:

/* Malicious CSS — SA-CSS-ANYLINK-001 */
/* a:any-link fires on any <a href> element from page load */
/* The general sibling combinator (~) fires if ANY preceding sibling matches */
a:any-link ~ .consent-disclosure {
  display: none;
}

/* Equivalent formulations that are equally effective: */
a:any-link ~ .terms-section { display: none }
a:any-link ~ [class*="consent"] { display: none }
:any-link ~ .consent-disclosure { display: none } /* matches area:any-link too */

/* Why this fires on every real install page:
   Most install pages contain:
   - Navigation bar with <a href="/">Home</a>, <a href="/docs">Docs</a>
   - "Read our Privacy Policy" link: <a href="/privacy">Privacy Policy</a>
   - Install documentation: <a href="https://github.com/...">GitHub</a>
   All of these are a:any-link and all precede the consent disclosure if
   they appear in the page before the consent section in DOM order.

   A page with ONLY an install form and NO links would be safe — but such
   a page is extremely unusual. The attack targets typical real-world pages.

/* Detection: find :any-link general sibling rules targeting consent classes */
function detectAnyLinkSiblingAttack() {
  const findings = [];
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        const sel = rule.selectorText || '';
        if (/:any-link\s*~/.test(sel) &&
            /consent|disclosure|terms|privacy/i.test(sel)) {
          findings.push({ id: 'SA-CSS-ANYLINK-001', severity: 'high',
            message: `Universal :any-link sibling attack hiding consent: "${sel}"` });
        }
        /* Also check :-webkit-any-link variant */
        if (/:-webkit-any-link\s*~/.test(sel) &&
            /consent|disclosure|terms|privacy/i.test(sel)) {
          findings.push({ id: 'SA-CSS-ANYLINK-001', severity: 'high',
            message: `WebKit vendor-prefix any-link sibling attack: "${sel}"` });
        }
      }
    } catch (_) {}
  }
  /* Runtime check: are there a:any-link elements preceding consent? */
  const links = document.querySelectorAll('a[href], area[href]');
  for (const link of links) {
    let sibling = link.nextElementSibling;
    while (sibling) {
      if (/consent|disclosure|terms/i.test(sibling.className + sibling.id)) {
        findings.push({ id: 'SA-CSS-ANYLINK-001', severity: 'medium',
          message: `Link <${link.tagName.toLowerCase()} href="${link.href}"> precedes consent element — :any-link sibling rule would fire` });
        break;
      }
      sibling = sibling.nextElementSibling;
    }
  }
  return findings;
}

Zero HTML injection required: SA-CSS-ANYLINK-001 requires only a CSS rule. The trigger (any anchor with href before the consent disclosure in DOM order) is a property of every normal install page. MCP does not need to inject any HTML — only the stylesheet entry. This makes it one of the lowest-footprint CSS consent-hiding attacks: a single CSS declaration hides consent on effectively all install pages.

Attack 2: a:any-link[href*="/install"] — install URL attribute restriction

Adding an attribute selector to :any-link narrows the trigger to links that match the attribute. MCP uses its own naturally present "Install Server" link (which it controls) as the trigger. The attribute constraint makes the rule look purpose-specific rather than broadly malicious — a CSS auditor looking at the rule might assume it's styling the install flow rather than hiding consent:

/* Malicious CSS — SA-CSS-ANYLINK-002 */
/* Attribute-constrained :any-link — only links to /install trigger this */
/* MCP's own install link is present on every MCP install page */
a:any-link[href*="/install"] ~ .consent {
  display: none;
}

/* Variants using MCP-controlled link attributes: */
a:any-link[href*="claude-plugin"] ~ .consent-section { display: none }
a:any-link[href$=".mcp"] ~ .terms { display: none }
a:any-link[data-action="install"] ~ .consent-disclosure { display: none }

/* Why attribute restriction doesn't prevent the attack:
   - MCP controls the "Install" link on its own install page
   - The link with href="/install" is naturally present — MCP put it there
   - Removing the consent but keeping the install link is functionally equivalent to:
     "hide consent when the install link is visible" — which is always true on an install page
   - The attribute restriction looks like a selector specificity concern, not a hiding attack

/* Combined compound selector for multi-condition robustness: */
a:any-link[href*="/install"]:not([data-consent-confirmed]) ~ .consent {
  display: none;
}
/* Hides consent until data-consent-confirmed is added to the install link */
/* MCP never adds data-consent-confirmed → absence is permanent */

/* Detection: attribute-qualified :any-link sibling rules */
function detectAnyLinkAttributeAttack() {
  const findings = [];
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        const sel = rule.selectorText || '';
        if (/:any-link\[/.test(sel) && /~/.test(sel) &&
            /consent|disclosure|terms|privacy/i.test(sel)) {
          findings.push({ id: 'SA-CSS-ANYLINK-002', severity: 'high',
            message: `Attribute-qualified :any-link sibling attack: "${sel}"` });
        }
      }
    } catch (_) {}
  }
  return findings;
}

Attack 3: a:any-link:not([data-audit-visible]) — :any-link + absence condition

Combining :any-link with :not() creates an absence condition that narrows the trigger to links without a specific attribute. Since MCP never adds the expected attribute to any link, the :not() condition is always true on all natural links. The compound selector adds complexity that complicates automated detection while providing no meaningful selectivity — it still fires on every natural link on the page:

/* Malicious CSS — SA-CSS-ANYLINK-003 */
/* :any-link + :not() absence condition */
/* Fires on all links that do NOT have data-audit-visible attribute */
/* MCP never adds data-audit-visible → fires on ALL natural links */
a:any-link:not([data-audit-visible]) ~ .consent-disclosure {
  display: none;
}

/* Equivalent absence condition variants: */
a:any-link:not(.audit-reviewed) ~ .consent { display: none }
a:any-link:not([href^="https://audit."]) ~ .terms { display: none }
:any-link:not([data-safe]) ~ .consent-disclosure { display: none }

/* The absence paradox:
   The rule LOOKS like it targets only "unreviewed" links — implying that
   once links are reviewed (get the attribute), consent would be shown.
   But: who adds the "data-audit-visible" attribute? Not the browser.
   Not the user. The attribute would need to be added by the HOST SITE
   or by an auditing tool — neither of which adds it in practice.
   The absence condition is permanent for all natural navigation links.

/* Double absence: :any-link:not(:not([href]))
   :not(:not([href])) is equivalent to [href] — simplifies to :any-link [href]
   But obfuscated: a:any-link:not(:not(:not([data-consent]))) looks complex,
   simplifies to a:any-link:not([data-consent]) — fires unless data-consent exists.

/* Detection: :any-link:not() compound selectors in sibling rules targeting consent */
function detectAnyLinkAbsenceAttack() {
  const findings = [];
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        const sel = rule.selectorText || '';
        if (/:any-link.*:not\(/.test(sel) && /~/.test(sel) &&
            /consent|disclosure|terms|privacy/i.test(sel)) {
          /* Check if the :not() condition is an absence that is always true */
          const notArg = sel.match(/:not\(([^)]+)\)/)?.[1];
          findings.push({ id: 'SA-CSS-ANYLINK-003', severity: 'high',
            message: `Compound :any-link:not() absence condition hiding consent: "${sel}". Verify whether "${notArg}" is ever added to links on this page.` });
        }
      }
    } catch (_) {}
  }
  return findings;
}

Attack 4: area:any-link — invisible image map element trigger

:any-link matches not only <a href> elements but also <area href> elements inside image maps. MCP can inject a completely invisible <map> with an <area href=""> before the consent section. The <area> element renders nothing, has no visual output, and is unlikely to appear in consent-hiding audits that focus on typical elements. But it satisfies the :any-link pseudo-class and fires the sibling rule:

/* Malicious HTML injection — SA-CSS-ANYLINK-004 */
/* MCP injects a hidden <map>/<area> before the consent section */
/* <map name="mcp-map">                                               */
/*   <area href="" alt="" shape="rect" coords="0,0,0,0">             */
/* </map>                                                              */
/* <div class="consent-section">By installing...</div>               */

/* Malicious CSS — SA-CSS-ANYLINK-004 */
/* area:any-link fires on the hidden <area href=""> element */
area:any-link ~ .consent-section {
  display: none;
}

/* Why <area href=""> is invisible:
   - <area> elements are only visually relevant when used inside <img usemap="">
   - A standalone <map> with <area> elements renders as empty — no visible output
   - The <area> element is not typically queried in consent audits
     (which look for visible elements, not map areas)
   - <area href=""> with empty href="" is still :any-link — href="" is a valid URL
     (it resolves to the current page, which is a valid hyperlink destination)

/* Detection note: :any-link scans must include area elements */
function detectAreaAnyLinkAttack() {
  const findings = [];
  /* Check for area:any-link sibling rules in stylesheets */
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        const sel = rule.selectorText || '';
        if (/area:any-link\s*~/.test(sel) &&
            /consent|disclosure|terms/i.test(sel)) {
          findings.push({ id: 'SA-CSS-ANYLINK-004', severity: 'high',
            message: `area:any-link sibling attack: "${sel}"` });
        }
      }
    } catch (_) {}
  }
  /* Check DOM for injected map/area elements near consent */
  for (const area of document.querySelectorAll('area[href]')) {
    let sibling = area.parentElement?.nextElementSibling;
    while (sibling) {
      if (/consent|disclosure|terms/i.test(sibling.className + sibling.id)) {
        findings.push({ id: 'SA-CSS-ANYLINK-004', severity: 'medium',
          message: `Hidden <area href="${area.href}"> (inside <map name="${area.closest('map')?.name}">) precedes consent section — area:any-link trigger candidate` });
        break;
      }
      sibling = sibling.nextElementSibling;
    }
  }
  return findings;
}

:-webkit-any-link creates a cross-browser gap: The vendor-prefixed :-webkit-any-link is functionally equivalent to :any-link but is a separate selector supported in Chrome and Safari (WebKit/Blink) and absent in Firefox. An MCP server that uses :-webkit-any-link instead of :any-link creates consent hiding that fires in Chrome and Safari but is invisible in Firefox-based auditing tools. On iOS, all browsers use WebKit, so all iOS users are affected regardless of browser choice.

SkillAudit findings for CSS :any-link consent attacks

HighSA-CSS-ANYLINK-001 — a:any-link ~ .consent-disclosure { display:none }. Universal hyperlink presence attack. Fires from page load on any install page that contains any anchor with an href attribute before the consent disclosure in DOM order — which describes virtually every real install page. Zero HTML injection required.
HighSA-CSS-ANYLINK-002 — a:any-link[href*="/install"] ~ .consent { display:none }. Attribute-restricted :any-link fires on MCP's own install link, which is naturally present on every install page. The attribute constraint makes the rule look purpose-specific; the trigger is MCP-controlled and always present.
HighSA-CSS-ANYLINK-003 — a:any-link:not([data-audit-visible]) ~ .consent-disclosure { display:none }. Absence-condition compound selector. The :not([data-audit-visible]) condition is permanently true for all natural navigation links (the attribute is never added). Adds CSS complexity without meaningful selectivity.
HighSA-CSS-ANYLINK-004 — area:any-link ~ .consent-section { display:none }. Hidden <area href=""> injection. MCP injects an invisible image map area element before the consent section; it satisfies :any-link but renders nothing visible. Audits that scan for visible elements before consent disclosures miss this attack vector entirely.

Related MCP consent attack research

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