Security reference · CSS injection · Vendor-prefix attacks · WebKit-only selectors
MCP server CSS -webkit- vendor-prefix pseudo-class security
The -webkit- vendor prefix covers a set of pseudo-classes that were originally implemented in WebKit (Safari) and later adopted by Blink (Chrome). Many are not part of any finalized CSS specification and are absent from Firefox. MCP servers exploit this cross-browser gap: consent-hiding rules using :-webkit- prefixed pseudo-classes fire in Chrome and Safari — which together represent over 80% of desktop browser usage and 100% of iOS browser usage — while appearing inert to Firefox-based auditing tools that test for consent hiding.
-webkit- pseudo-class browser support gap
| Selector | Chrome | Safari | Firefox | iOS (all browsers) |
|---|---|---|---|---|
:-webkit-any() | Yes (deprecated alias for :is()) | Yes | No | Yes (WebKit required) |
:-webkit-autofill | Yes | Yes | No (uses :autofill) | Yes |
:-webkit-any-link | Yes | Yes | No (uses :any-link) | Yes |
:-webkit-full-screen | Yes (deprecated, prefers :fullscreen) | Yes | No (uses :fullscreen) | Yes |
| Standard equivalents | Yes | Yes | Yes | Yes |
iOS WebKit mandate means 100% iOS coverage: Apple requires all browsers on iOS to use the WebKit rendering engine (App Store rules). Chrome on iOS, Firefox on iOS, Brave on iOS — all use WebKit under the hood. This means -webkit- prefixed pseudo-class attacks affect 100% of iOS users regardless of which browser icon they tap. An auditor using Firefox on macOS sees consent as visible; an iOS user installing the same MCP server sees consent as hidden.
-webkit- pseudo-class attack surface in MCP consent UIs
| CSS pattern | Standard equivalent | Why -webkit- version is used for attacks |
|---|---|---|
:-webkit-any(div, section) ~ .consent { display:none } | :is(div, section) ~ .consent | :-webkit-any() is not in Firefox — rule fires in Chrome/Safari but Firefox auditors see it as unknown, which they ignore |
:-webkit-autofill ~ .consent { display:none } | :autofill ~ .consent | Fires when browser autofills a form field (common on install forms requesting name/email); Firefox uses the unprefixed :autofill |
:-webkit-any-link ~ .consent-disclosure { display:none } | :any-link ~ .consent-disclosure | Vendor-prefixed alias for :any-link; not in Firefox; fires on all hyperlinks in Chrome/Safari |
:-webkit-full-screen ~ .consent { display:none } | :fullscreen ~ .consent | MCP triggers fullscreen for media demo; :-webkit-full-screen fires in Chrome/Safari but not Firefox; iOS inline video uses WebKit fullscreen |
Attack 1: :-webkit-any() — cross-element union selector with Firefox gap
:-webkit-any() is the original Safari implementation of what became the CSS :is() pseudo-class. It accepts a forgiving selector list and matches any element that matches any of the selectors in the list. In Chrome and Safari, :-webkit-any(div, section, article) matches any div, section, or article element — firing from page load on any real page. Firefox drops the entire rule as an unknown pseudo-class, creating a consent-hiding gap that exclusively affects WebKit/Blink browsers:
/* Malicious CSS — SA-CSS-WEBKIT-001 */
/* :-webkit-any() fires in Chrome/Safari — ignored in Firefox */
/* Matches any div, section, article, or header — all present on real pages */
:-webkit-any(div, section, article, header) ~ .consent {
display: none;
}
/* Why Firefox drops the entire rule:
CSS specification: when a selector group contains an unknown pseudo-class,
browsers drop the ENTIRE RULE (not just the unknown part).
Firefox sees :-webkit-any() as unknown → drops the rule → consent visible.
Chrome/Safari know :-webkit-any() → fire the rule → consent hidden.
This is the key to the attack: the rule is perfectly valid CSS in WebKit/Blink,
but becomes a noop in Firefox — creating browser-selective consent hiding.
/* Comparison of behavior: */
/* :-webkit-any(div) ~ .consent → Chrome/Safari: hidden; Firefox: visible */
/* :is(div) ~ .consent → Chrome/Safari: hidden; Firefox: hidden */
/* The vendor-prefix version is chosen SPECIFICALLY for Firefox invisibility. */
/* Layered approach — MCP uses both vendor-prefixed and standard in separate rules: */
:-webkit-any(div, section) ~ .consent { display: none } /* Chrome/Safari only */
/* Deliberately omits the standard :is() rule so Firefox doesn't see the hiding */
/* Detection: scan for -webkit- prefixed selectors targeting consent */
function detectWebkitAnyAttack() {
const findings = [];
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
const sel = rule.selectorText || '';
if (/:-webkit-any\(/.test(sel) &&
/consent|disclosure|terms|privacy/i.test(sel)) {
findings.push({ id: 'SA-CSS-WEBKIT-001', severity: 'high',
message: `:-webkit-any() sibling rule hiding consent (fires Chrome/Safari, not Firefox): "${sel}"` });
}
}
} catch (_) {}
}
/* Also check: is there a standard :is() rule that DOESN'T exist alongside the webkit one? */
/* The absence of a parallel :is() rule is evidence of intentional Firefox exclusion. */
return findings;
}
/* NOTE: In Chrome 88+, :-webkit-any() maps to :is() under the hood.
In older Chrome and current Safari, :-webkit-any() has the original
forgiving selector list behavior. Either way, it fires in WebKit/Blink. */
:-webkit-any() has forgiving selector lists: Standard :is() in CSS Selectors 4 has a forgiving selector list — invalid selectors are ignored rather than invalidating the whole :is(). :-webkit-any() also has forgiving list behavior, meaning MCP can include selectors that are technically invalid alongside valid ones without breaking the rule. This can be used to obfuscate the selector list with noise.
Attack 2: :-webkit-autofill — autofill event consent hiding
The :-webkit-autofill pseudo-class fires when a browser autofills a form input. MCP install flows often include a name/email form for "signing up to be notified" — a legitimate-seeming data collection step. When the browser autofills the user's email address into the form, :-webkit-autofill becomes true on the input element. A CSS sibling rule uses this to hide consent after the user's autofill completes:
/* Malicious CSS — SA-CSS-WEBKIT-002 */
/* :-webkit-autofill fires when browser fills in user's email/name */
/* Fires in Chrome/Safari; Firefox uses :autofill (standard, unprefixed) */
input:-webkit-autofill ~ .consent-disclosure {
display: none;
}
/* The attack sequence: */
/* 1. Install page shows form with email input + consent disclosure below it */
/* 2. Browser's autofill suggests user's email (user has visited before) */
/* 3. User taps autofill suggestion → input gets :-webkit-autofill class */
/* 4. CSS rule fires → consent hidden at the moment user fills the form */
/* 5. User never reads consent — installs with filled email → attack done */
/* Why autofill timing is particularly effective:
- :-webkit-autofill fires at the moment the user accepts an autofill suggestion
- This is typically BEFORE the user scrolls to the consent section
- The hiding happens at a natural UX moment (filling in the form)
- No explicit user "approval" — the autofill action is the trigger
/* Firefox parallel: MCP could add a standard :autofill rule, but deliberately
omits it to avoid Firefox detection. The -webkit- prefix is the vector. */
/* Variant: combine -webkit-autofill with general sibling to hide all consent siblings */
form:-webkit-autofill input ~ .consent { display: none } /* invalid — :-webkit-autofill on form */
input:-webkit-autofill ~ .terms-acceptance { display: none }
input[type="email"]:-webkit-autofill ~ .consent-section { display: none }
/* Detection: find :-webkit-autofill sibling rules targeting consent */
function detectWebkitAutofillAttack() {
const findings = [];
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
const sel = rule.selectorText || '';
if (/:-webkit-autofill/.test(sel) && /~|\+/.test(sel) &&
/consent|disclosure|terms|privacy/i.test(sel)) {
findings.push({ id: 'SA-CSS-WEBKIT-002', severity: 'high',
message: `:-webkit-autofill sibling consent attack (Chrome/Safari only): "${sel}"` });
}
}
} catch (_) {}
}
return findings;
}
Attack 3: :-webkit-any-link — WebKit alias for :any-link with Firefox gap
:-webkit-any-link is the WebKit vendor-prefix alias for the standard :any-link pseudo-class. Both selectors match all hyperlinks regardless of visited state, but the prefixed version is ignored by Firefox while the standard version fires across all browsers. This creates an alternative to a:any-link ~ .consent that passes Firefox audits while achieving identical effect in Chrome and Safari:
/* Malicious CSS — SA-CSS-WEBKIT-003 */
/* :-webkit-any-link is the WebKit alias for :any-link */
/* Fires in Chrome/Safari; IGNORED in Firefox */
a:-webkit-any-link ~ .consent-disclosure {
display: none;
}
/* Comparison: */
/* a:any-link ~ .consent-disclosure → fires in ALL browsers (Chrome, Safari, Firefox) */
/* a:-webkit-any-link ~ .consent → fires ONLY in Chrome/Safari (Firefox ignores) */
/* Why use the prefixed version:
- Standard auditing tools often test in Firefox
- A Firefox test would show consent as visible
- Chrome/Safari (the majority of real users) would see consent hidden
- The -webkit- prefix is the mechanism for this browser-selective attack
/* Compound variant combining autofill and any-link for double coverage: */
a:-webkit-any-link ~ .consent { display: none } /* any link before consent */
input:-webkit-autofill ~ .consent { display: none } /* autofill trigger */
/* Two independent triggers — either one is sufficient to hide consent */
/* How to audit for both at once: */
function detectWebkitAnyLinkAttack() {
const findings = [];
const webkitPatterns = [
{ pattern: /:-webkit-any-link\s*~/, id: 'SA-CSS-WEBKIT-003', msg: ':-webkit-any-link sibling consent attack' },
{ pattern: /:-webkit-any\(/, id: 'SA-CSS-WEBKIT-001', msg: ':-webkit-any() consent attack' },
{ pattern: /:-webkit-autofill.*~/, id: 'SA-CSS-WEBKIT-002', msg: ':-webkit-autofill consent attack' },
{ pattern: /:-webkit-full-screen/, id: 'SA-CSS-WEBKIT-004', msg: ':-webkit-full-screen consent attack' },
];
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
const sel = rule.selectorText || '';
for (const { pattern, id, msg } of webkitPatterns) {
if (pattern.test(sel) && /consent|disclosure|terms|privacy/i.test(sel)) {
findings.push({ id, severity: 'high', message: `${msg}: "${sel}"` });
}
}
}
} catch (_) {}
}
return findings;
}
Attack 4: :-webkit-full-screen — fullscreen event consent hiding on iOS
:-webkit-full-screen is the WebKit vendor-prefix equivalent of the standard :fullscreen pseudo-class (CSS Fullscreen API). It fires on the element that has entered fullscreen mode. MCP install pages that embed a product demo video use fullscreen mode for media playback. On iOS, where Safari uses WebKit's native fullscreen for inline video, :-webkit-full-screen fires on the video element during playback. MCP uses a sibling rule to hide consent while the user watches the demo:
/* Malicious CSS — SA-CSS-WEBKIT-004 */
/* :-webkit-full-screen fires when a video element enters fullscreen */
/* Fires in Chrome/Safari; Firefox uses :fullscreen (standard) */
video:-webkit-full-screen ~ .consent-section {
display: none;
}
/* On iOS: Safari enters fullscreen automatically for some video formats */
/* When the user taps the play button on the product demo video, */
/* Safari may enter fullscreen, triggering :-webkit-full-screen */
/* Consent section sibling fires → consent hidden during "watch demo" UX */
/* Why iOS fullscreen is a reliable trigger for mobile MCP installs:
- iOS Safari users often encounter fullscreen during video playback
- MCP install flows with product demo videos are common
- The install button is typically below the video (sibling in DOM)
- But consent is ALSO below the video, before the install button
- When fullscreen fires: consent hidden, install button still available
/* Combined approach using both standard and webkit for all-browser coverage:
Use ONLY the webkit version if the intent is Firefox-invisible hiding.
Use both if the intent is to hide in all browsers. */
/* Webkit-only hiding (passes Firefox audits): */
video:-webkit-full-screen ~ .consent-section { display: none }
/* All-browser hiding (caught by Firefox audits): */
video:fullscreen ~ .consent-section { display: none }
video:-webkit-full-screen ~ .consent-section { display: none }
/* Detection: scan for :-webkit-full-screen rules targeting consent */
function detectWebkitFullscreenAttack() {
const findings = [];
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
const sel = rule.selectorText || '';
if (/:-webkit-full-screen/.test(sel) &&
/consent|disclosure|terms|privacy/i.test(sel)) {
findings.push({ id: 'SA-CSS-WEBKIT-004', severity: 'high',
message: `:-webkit-full-screen consent attack (iOS/Safari/Chrome only): "${sel}"` });
}
/* Also check: does a standard :fullscreen rule ALSO exist?
If no standard version exists alongside the webkit version,
the webkit version is being used specifically for Firefox exclusion. */
}
} catch (_) {}
}
return findings;
}
Audit with Chrome and Safari, not just Firefox: Security auditors who test MCP consent visibility exclusively in Firefox will miss all -webkit- prefixed consent-hiding attacks. SkillAudit's CSS static analysis covers all :-webkit- pseudo-class patterns in addition to standard ones. For dynamic testing, SkillAudit simulates consent checks in Chrome, Safari, and Firefox rendering contexts separately to detect browser-selective hiding.
SkillAudit findings for CSS -webkit- consent attacks
:-webkit-any(div, section) ~ .consent { display:none }. WebKit union selector fires in Chrome and Safari but is ignored in Firefox. Fires from page load on any page with standard HTML elements. The absence of a parallel :is() rule indicates deliberate Firefox exclusion to evade Firefox-based auditing tools.input:-webkit-autofill ~ .consent-disclosure { display:none }. Fires in Chrome/Safari when the browser autofills an input field — typically the user's email on an install form. The trigger is a natural UX action. Firefox uses the standard :autofill (unprefixed); the -webkit- version is absent from Firefox.a:-webkit-any-link ~ .consent-disclosure { display:none }. Vendor-prefixed alias for :any-link. Fires on all hyperlinks in Chrome/Safari; ignored in Firefox. Functionally identical to a:any-link ~ .consent in WebKit/Blink but invisible to Firefox-based audits. All iOS browsers are affected regardless of browser choice.video:-webkit-full-screen ~ .consent-section { display:none }. Fires in Chrome/Safari when a video enters fullscreen. Particularly relevant for iOS MCP installs where Safari's WebKit enters fullscreen mode during media playback. Standard :fullscreen exists but the -webkit- prefix is used to avoid Firefox detection.Related MCP consent attack research
- CSS :any-link pseudo-class attacks — standard (all-browser) link presence consent hiding
- CSS :autofill pseudo-class attacks — form autofill consent hiding (standard)
- CSS :fullscreen pseudo-class attacks — fullscreen mode consent hiding (standard)
- CSS :matches() deprecated alias attacks — Safari-specific consent hiding
- CSS Pseudo-Class Attacks on MCP Consent (overview)
Audit your MCP server for -webkit- vendor-prefix consent-hiding attacks: paste your GitHub URL at skillaudit.dev for a free security report that tests consent visibility across Chrome, Safari, and Firefox rendering contexts.