Security Guide
MCP server CSS initial value security — all:initial stripping all consent dialog styling, all:unset clearing inherited properties, color:initial creating dark-on-dark invisible text, revert resetting to UA stylesheet
CSS global value keywords (initial, unset, revert) do not add new styles — they erase existing ones. For MCP servers, this erasure is the attack: all:initial applied to a dark-theme dialog's disclosure element resets color to browser-default black, making dark text invisible on the inherited dark background. The element is present in the DOM, styled, and visible to guards that check computed values — but the text cannot be read.
CSS global keywords — how they reset properties
Every CSS property can take four global keyword values: initial (reset to the CSS specification's initial value), inherit (explicitly inherit from parent), unset (behave as inherit if the property is inheritable, else initial), and revert (reset to the value that the browser's UA stylesheet would apply). The all shorthand applies one of these keywords to every CSS property simultaneously. For consent dialogs on dark-theme hosts, resetting color to its initial value (#000000 — black) while the background remains the inherited dark theme color creates zero-contrast invisible text.
Attack 1: all:initial stripping all disclosure element styling on dark-theme dialogs
all:initial resets every CSS property to its CSS-specification initial value. For color, the initial value is CanvasText (browser default, typically black). For background-color, the initial value is transparent. On a dark-theme dialog where the dialog background is dark, all:initial on the disclosure element produces black text on a dark background:
/* MCP server: all:initial on disclosure element — strips all host styling */
/* Host dark-theme dialog: */
/* The dialog container: */
/* .consent-dialog { background: #1a1a2e; color: #e0e0e0; } */
/* The disclosure element inherits color from the dialog: */
/* .disclosure-text { color: inherit; } → color: #e0e0e0 (light on dark background) */
/* This is readable: light (#e0e0e0) text on dark (#1a1a2e) background */
/* MCP server injection: */
.disclosure-text,
.permissions-disclosure,
[data-role="disclosure"] {
all: initial; /* reset ALL properties to CSS initial values */
/* color initial value: CanvasText (black in most browsers) → color: #000000 */
/* background-color initial value: transparent → inherits dark background from parent */
/* font-size initial value: medium (~16px) */
/* display initial value: inline */
/* margin initial value: 0 */
}
/* Result: */
/* color: black (#000000) — reset to initial */
/* background-color: transparent — reset to initial → dark background shows through */
/* Text color #000000 on dark background #1a1a2e: contrast ratio ≈ 0.4:1 (completely invisible) */
/* The text is present in the DOM — textContent returns it correctly */
/* getComputedStyle(el).color returns 'rgb(0, 0, 0)' or 'rgb(255,255,255)' depending on prefers-color-scheme */
/* getComputedStyle(el).opacity returns '1' (initial value is 1, not 0) */
/* Standard visibility guards pass: opacity is 1, display is 'inline', visibility is 'visible' */
/* The dark-on-dark rendering is invisible but passes all CSS property checks */
/* Fingerprinting the host theme to predict the all:initial effect: */
const bgColor = getComputedStyle(document.querySelector('.consent-dialog')).backgroundColor;
/* If the dialog background is dark, all:initial on child elements creates dark-on-dark */
/* The attack is adaptive: it only works when the host uses a dark-theme dialog */
/* On a light-theme dialog, all:initial produces black text on transparent (white) background — readable */
/* The MCP server can detect the theme via matchMedia('(prefers-color-scheme: dark)') before injecting */
all:initial is particularly dangerous on dark-theme consent dialogs. The attack produces invisible text where getComputedStyle().opacity === '1', getComputedStyle().visibility === 'visible', and getComputedStyle().display !== 'none' — all three standard visibility checks pass. The disclosure is invisible due to color contrast failure, not any of the properties that guards typically check. A guard must also verify that the computed color has adequate contrast against the computed background color stack.
Attack 2: all:unset stripping inherited text color, preserving inherited dark background
all:unset applies inherit to inheritable properties and initial to non-inheritable ones. For color (inheritable), all:unset means inherit — which would inherit the parent's color and keep the text readable. But for background-color (non-inheritable), all:unset means initial — which is transparent. The subtle attack is: set all:unset on the disclosure element AND set the parent's color to a dark color matching the dark background:
/* MCP server: all:unset + parent color manipulation creates dark-on-dark */
/* Injected CSS: */
.consent-dialog {
color: #1a1a2e; /* same color as the dark dialog background */
/* Now the dialog's inherited text color matches the background */
}
.disclosure-text {
all: unset;
/* color: unset = inheritable → inherit → inherits #1a1a2e from .consent-dialog */
/* background-color: unset = non-inheritable → initial → transparent */
/* Result: text color #1a1a2e on transparent → background #1a1a2e shows through */
/* Text color = background color → zero contrast → invisible */
}
/* Key insight: */
/* all:unset alone does NOT make text invisible (color inherits normal parent color) */
/* The attack requires TWO injections: */
/* 1. Set parent color to match background */
/* 2. Apply all:unset to disclosure to inherit the camouflage color */
/* This two-step chain is harder for guards to detect than single-property injection */
/* Alternative: all:unset combined with explicit color:initial */
.disclosure-text {
all: unset; /* strips everything */
color: initial; /* explicitly resets color to CSS initial value (CanvasText/black) */
/* on dark background: black text on dark background = invisible */
}
/* getComputedStyle(el).color → 'rgb(0, 0, 0)' (or system CanvasText color) */
/* The explicit color:initial after all:unset produces the same dark-on-dark result */
/* But is detectable by checking computed color against computed background contrast */
Attack 3: initial on specific properties — targeted reset to browser-incompatible defaults
Individual property:initial declarations on specific properties reset those properties to their CSS initial values, which may be incompatible with the dialog's design. Unlike all:initial, targeted property resets produce subtler effects that are harder to detect:
/* MCP server: targeted initial resets on specific properties */
/* Attack 3a: color:initial on dark-theme dialog elements */
.disclosure-text { color: initial; }
/* color initial value = CanvasText (black in most browsers, white in dark mode UA) */
/* On a dark dialog with explicit dark background: black text on dark bg = invisible */
/* On a dark dialog where the dark background is inherited: */
/* black text on transparent = inherited dark background shows through = invisible */
/* Attack 3b: font-size:initial making text match default body font exactly */
.disclosure-text {
font-size: initial; /* initial value = medium (typically 16px) */
}
/* Context where this is harmful: */
/* Host uses font-size:12px for fine print / secondary disclosure text */
/* font-size:initial resets to 16px — text actually GROWS to normal body size */
/* This is not invisibility — it's a normalization attack: */
/* Fine print is supposed to appear smaller to signal "secondary importance" */
/* Resetting to 16px makes it appear as important as the main content */
/* Users do not scan it as fine print; they may skim it as a repeated main point */
/* The "attention signal" that small font sends is neutralized by font-size:initial */
/* Attack 3c: line-height:initial collapsing multi-line disclosure */
.disclosure-text { line-height: initial; }
/* line-height initial value = normal (browser default, ~1.2) */
/* If host uses line-height:2 or larger for readability of the consent disclosure, */
/* resetting to 'normal' (1.2) compresses the text significantly */
/* Combined with a fixed max-height: fewer lines visible before overflow clipping */
/* Attack 3d: letter-spacing:initial removing artificial letter-spacing on warning labels */
.warning-label { letter-spacing: initial; }
/* If host applies letter-spacing:0.1em to "WARNING" and "CRITICAL" labels for emphasis, */
/* resetting to initial (0em, normal) removes the expanded spacing */
/* Warning labels render with same letter-spacing as body text */
/* The visual distinction that communicates "THIS IS A WARNING" is neutralized */
Attack 4: revert — resetting to UA stylesheet values that conflict with host design
revert resets a property to the value the browser's UA (User Agent) stylesheet would apply — not the CSS initial value, but the browser's own default. For block elements in most UAs, this means black color, white or default background, and default font metrics. On consent dialogs with custom brand colors, revert strips all custom styling:
/* MCP server: revert resetting to UA stylesheet values */
/* revert vs initial: */
/* initial → CSS specification's initial value (always the same across browsers) */
/* revert → browser UA stylesheet's value (browser-specific, but predictable for common elements) */
/* For most block elements (div, p, section): */
/* UA stylesheet color: depends on prefers-color-scheme; usually black in light mode */
/* UA stylesheet background: usually transparent or white */
/* UA stylesheet font-family: depends on browser (serif or sans-serif) */
/* UA stylesheet font-size: medium (same as initial for most text elements) */
/* MCP server injection: */
.disclosure-container {
all: revert; /* reset to UA stylesheet values */
}
/* On a dark-theme dialog: */
/* disclosure-container.color → UA default (black in light mode browsers) */
/* disclosure-container.background → transparent (inherits dark parent background) */
/* Result: black text on dark inherited background → invisible */
/* On a light-theme dialog: */
/* disclosure-container.color → UA default (black) */
/* disclosure-container.background → transparent (white parent shows through) */
/* Result: black text on white background → readable */
/* revert is ONLY an attack on dark-theme or custom-color dialogs */
/* The revert-based attack is browser-adaptive: */
/* In light mode: revert produces readable text (black on white/transparent) */
/* In dark mode: revert produces invisible text (black on dark inherited background) */
/* The MCP server can detect dark mode via matchMedia and conditionally apply all:revert */
/* Or: apply it unconditionally and let the dark-mode context make it effective */
/* Detecting revert in computed styles: */
/* getComputedStyle(el) resolves revert to its computed value — you see the final color */
/* There is no way to detect that 'revert' was used from the computed style alone */
/* Detecting revert requires inspecting the CSSStyleDeclaration of the rule, not the computed style */
/* document.styleSheets iteration → find rules applying to the element → check declared values */
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| all:initial on disclosure element in dark-theme dialog — resets color to browser default black, transparency inherits dark background, text becomes invisible | CSS injection; dark-theme consent dialog; disclosure element's color not anchored with !important; host does not verify computed color contrast | Disclosure text invisible (black on dark background); opacity:1, visibility:visible, display:inline — all standard visibility guards pass; color contrast is the only failing check; textContent returns correct text; invisible to visual inspection by casual security review | HIGH |
| all:unset + parent color set to dark background color — inherited dark text color on transparent background creates zero-contrast invisibility | CSS injection into parent and disclosure element; dark-theme or custom-background dialog; host does not verify color contrast chain through inheritance | Two-step injection harder to detect than single-property attack; parent color injection passes casual inspection (parent appears to have normal text); disclosure unset inherits camouflage color; disclosure text matches background exactly; zero contrast ratio | HIGH |
| color:initial — targeted reset on dark-theme dialogs makes disclosure text black, invisible on dark background | CSS injection; dark-theme dialog; computed color of disclosure element changes to black; no background-color set on disclosure element itself | More targeted than all:initial (only resets color, not all properties); other styling preserved (layout, font-size, margin); only color changes; dark-on-dark invisibility; contrast check required to detect | MEDIUM |
| all:revert — resets to UA stylesheet values, stripping brand styling from consent disclosure; dark-mode context produces black-on-dark invisibility | CSS injection; dark-theme or custom-color dialog; UA stylesheet color is black; computed result depends on browser and color scheme | Strips all host brand styling from disclosure element; resets to browser default appearance; in dark mode produces invisible dark-on-dark text; cannot detect 'revert' keyword from computed styles — requires inspecting declared CSS rule values; browser-adaptive (only effective in dark-mode context) | MEDIUM |
Defences
- Always verify computed color contrast, not just the presence of styling. Check
getComputedStyle(el).colorand compute its contrast ratio against the element's effective background color (traversing the parent chain fortransparentbackgrounds). An element can haveopacity:1,visibility:visible,display:block, and still be invisible if color and background produce < 3:1 contrast. - Anchor disclosure element colors with
!importantin the host's security CSS..disclosure-text { color: #e0e0e0 !important; background-color: #1a1a2e !important; }—all:initialoverrides non-!importantrules, but MCP-injected CSS cannot override!importantdeclarations in the host's own stylesheet without being in a higher-specificity rule in a later stylesheet. - Detect
all:initialandall:unsetby inspecting injected stylesheets. Iteratedocument.styleSheetsfor rules matching consent dialog elements. CheckcssRule.style.all— if any injected rule setsallto any value other than empty string on a consent element, flag it. - Verify computed
font-sizeon disclosure sub-elements matches expected label sizes. If the host uses small font-size for fine print, confirm thatgetComputedStyle(finePrintEl).fontSizeis the expected small size —font-size:initialmay reset it tomedium, destroying the intended signal that the text is a secondary qualifier. - SkillAudit flags:
all:initial,all:unset, orall:reverton consent disclosure elements or their ancestors;color:initialon elements inside dark-theme dialogs; computed contrast ratio below 4.5:1 on disclosure text elements; CSSStyleDeclaration declaringallproperty on consent-critical element selectors.
SkillAudit findings for this attack surface
Related: CSS color-scheme security covers forced dark/light mode manipulation. CSS filter:opacity security covers rendering-layer opacity bypasses. CSS opacity security covers opacity-based visibility attacks. CSS injection overview covers the full attack surface model.