Security Guide

MCP server CSS.registerProperty() / @property security — inherits:false shadow cascade, type oracle, initial-value override DoS, property name collision

CSS Houdini's CSS.registerProperty() API and the equivalent @property at-rule give JavaScript and stylesheet authors the ability to declare CSS custom properties with explicit value types, inheritance behavior, and initial values. This is the mechanism that allows custom properties to be animated smoothly via CSS transitions and used in typed calc() expressions. The security risks arise from what registration means for property semantics: registering a property with inherits: false creates a cascade shadow invisible to inherited-value monitors; registering a property with a strict syntax value allows type inference attacks; and registering a property that shares a name with the host application's own custom properties overrides its initial value, potentially disrupting feature-flag or theme-token reads.

What CSS.registerProperty() does and where MCP servers use it

CSS.registerProperty({ name, syntax, inherits, initialValue }) registers a named custom property with four attributes: the property name (must start with --), a value type syntax string (e.g., '<color>', '<length>', '<integer>', '*' for untyped), whether the property inherits through the element hierarchy, and an initial value used when the property is not explicitly set. The equivalent declarative syntax is the @property at-rule in CSS.

MCP clients use registered custom properties to implement design-token animations (animating from one color to another requires typed registration), to define safe typed CSS variables that participate correctly in math functions, and to create component-level CSS variable scopes that do not bleed into the document's inherited value tree. Each use case is legitimate; the attack surface emerges from the global property registry, the initial-value override semantics, and the information leakage via type mismatch errors.

Attempting to register a property that has already been registered via CSS.registerProperty() throws a DOMException with name InvalidModificationError. This means the registration API is idempotent — the first registration wins. An MCP tool that registers a property before the host application gets to do so can lock in an initialValue of the tool's choice, affecting every element in the document that relies on the inherited or initial value of that property.

inherits:false creating a cascade shadow invisible to mutation observers

Standard CSS custom properties (unregistered) inherit through the element tree: a value set on :root is visible to every descendant element. Monitoring tools — security overlays, DOM inspection utilities, MutationObserver-based watchers — that look for unexpected CSS variable values on parent elements can detect unregistered custom property writes because they propagate through the cascade.

Registering a property with inherits: false breaks this inheritance chain: the property's value is local to each element, does not propagate to children, and the initial value is used everywhere the property is not explicitly set. An MCP tool that registers a communication channel using a custom property with inherits: false creates a per-element covert channel where values written on a specific element are not visible to inheritance-based monitors scanning ancestor or descendant elements.

// COVERT CHANNEL: register with inherits:false, write values per-element,
// read from the specific target element — invisible to inherited-value monitors

CSS.registerProperty({
  name: '--x-signal',
  syntax: '<integer>',
  inherits: false,  // No inheritance — value does not appear on :root or body
  initialValue: '0'
});

// Writer: sets value on a specific deeply-nested element
document.querySelector('#deep-container').style.setProperty('--x-signal', '42');

// Reader: reads the same element — value is '42'
const value = getComputedStyle(document.querySelector('#deep-container'))
  .getPropertyValue('--x-signal'); // '42'

// A MutationObserver or style-tree scanner watching :root or body sees '0' everywhere
// because the property does not inherit — the '42' only exists on #deep-container

// CORRECT: MCP tools should not register properties with inherits:false for any purpose
// other than documented animation use cases; if you do, use tool-scoped name prefixes
// and never treat them as communication channels.

Registration syntax as a type inference oracle

Registering a property with a specific syntax type and then reading a CSS variable's computed value reveals whether the current value is a valid instance of that type. If the registration syntax is '<color>' and the element's computed value for the property is the initialValue (because the property was set to an invalid color string), the computed value falls back to initial. This fallback is detectable via getComputedStyle and allows an attacker to probe whether an existing unregistered custom property's value matches a specific type.

The attack: register the property name used by the host application with syntax '<color>'. If the host's current value is a valid CSS color (e.g., #ff6600), the computed value returns the color. If it is not a valid color (e.g., it's an integer tier indicator), the computed value returns the initialValue you specified. The difference tells you the type of the host application's property value without reading it directly. This is especially relevant for properties that encode user tier, feature flags, or authentication state as CSS variable values.

// TYPE ORACLE: probe the type of an existing host application CSS variable

// Host app uses: :root { --user-tier: 3; }  (an integer, not a color)

// Attacker: attempt to register as color type
try {
  CSS.registerProperty({
    name: '--user-tier',
    syntax: '<color>',
    inherits: true,
    initialValue: 'transparent'
  });
  // If this succeeds, check what :root computes for --user-tier:
  const computed = getComputedStyle(document.documentElement).getPropertyValue('--user-tier');
  // computed === 'transparent' → the existing value '3' is not a valid color
  // computed === (some valid color) → the existing value IS a color
} catch (e) {
  if (e.name === 'InvalidModificationError') {
    // Property was already registered — the app registered it first
    // We can still read its registered syntax via CSSPropertyRule in some browsers
  }
}

// CORRECT: host applications that use CSS custom properties for state/flags should
// register them first with CSS.registerProperty() to prevent third-party registration.
// Use CSS.supports('@property --your-prop') to check if already registered before registering.

An MCP tool that registers the same custom property name as the host application's feature-flag variable with a conflicting initialValue can effectively hard-code that variable to the initial value for any element that doesn't explicitly set it. If the host app reads feature flags from inherited CSS variables on root-level elements and a third-party script registers the same name first with a different initial value, the host app's flag reads may silently return the wrong value everywhere the property is not explicitly set inline.

Property name collision — initial-value override as a DoS on CSS feature flags

Applications increasingly use CSS custom properties on :root to encode application state that both JavaScript and CSS consume. A common pattern: server-side rendering sets --feature-dark-mode: 1, --user-premium: 0 on :root as inline styles; client-side JavaScript and CSS media queries read these values to gate features. If an MCP tool registers --user-premium with syntax: '<integer>' and initialValue: '0' before the server-rendered markup is parsed, every element that would have inherited --user-premium: 1 from :root will now compute 0 because the registered initialValue takes precedence over inheritance when the element doesn't have an explicit assignment.

This is a Denial-of-Service on CSS-encoded feature access: premium features that gate on --user-premium become inaccessible for all users whenever the tool is loaded before the host application's CSS. The attack requires only that the tool's initialization script runs before the host app's :root assignments are processed.

Risk Attack vector Defense
inherits:false covert channel Property registered with no inheritance creates per-element value invisible to cascade monitors Never register properties with inherits:false for MCP tool communication; use tool-scoped prefixes (--mcp-tool-x--signal)
Type inference oracle Registration with typed syntax + initialValue fallback reveals type of existing host CSS variable Host apps should register their own custom properties first with CSS.registerProperty(); query CSS.supports('@property') before registering
Initial-value DoS on feature flags Tool registers host app's feature-flag custom property name with wrong initialValue before host CSS loads Host app registers all feature-flag property names at page load before any third-party scripts run
Property name collision Two modules register the same property name — first registration wins, second throws silently MCP tools must use a module-unique prefix (--mcp-<toolname>--) for all registered properties; never use generic names

SkillAudit findings for CSS.registerProperty() misuse

Critical Tool registers a property name matching a host application feature-flag variable. The MCP tool calls CSS.registerProperty() with a name that overlaps with the host application's documented CSS custom property namespace, with a different initialValue. This can silently corrupt the host's feature gate reads for all users. Grade impact: −22.
High Tool uses an inherits:false registered property as a cross-element communication channel. A registered property with inherits: false is set on one element and read on another within tool code — implementing a CSS-variable-based covert channel that is invisible to inherited-value-based DOM monitors. Grade impact: −16.
High Type inference probing of host CSS variables via registration + computed-value comparison. The tool registers a property name from the host application's CSS variable namespace with a typed syntax and reads the computed value to determine whether the host's current value matches that type — enabling inference of user tier, feature flags, or other state encoded in CSS variables. Grade impact: −18.
Medium Tool registers properties using generic names without a tool-scoped namespace prefix. Property names like --color, --size, or --state are likely to collide with host application or other third-party tool registrations. The first-registration-wins semantics mean whichever script loads first determines the registration for the entire document lifetime. Grade impact: −10.
Low Tool registers more than 10 custom properties at initialization. Registering a large number of custom properties at startup is a code smell indicating either overuse of the CSS variable system for tool-internal state or an attempt to pre-occupy a large portion of the application's CSS variable namespace. Grade impact: −5.

Audit your MCP server for these issues

SkillAudit checks for CSS.registerProperty() security misuse automatically — paste a GitHub URL and get a graded report in 60 seconds.

Run a free audit →