Security Guide

MCP server CSS Typed Object Model security — computedStyleMap custom property enumeration, CSSUnitValue precision oracle, attributeStyleMap mutation bypass, typed value inference

The CSS Typed Object Model — element.attributeStyleMap, element.computedStyleMap(), CSSUnitValue, CSSMathValue — provides a structured, type-safe alternative to the string-based CSSOM. In MCP server tool contexts, it introduces four attack surfaces not present in the classic CSSOM: bulk enumeration of all CSS custom properties (including application secrets encoded in CSS variables), hardware fingerprinting via unit value conversion arithmetic, bypassing mutation observer monitoring tuned to watch classic CSSOM writes, and inferring which CSS properties the application computes dynamically from their typed value category. This page covers all four.

computedStyleMap() as CSS custom property bulk enumerator

element.computedStyleMap() returns a StylePropertyMapReadOnly object representing all computed CSS properties on an element — not just the ones explicitly set, but all resolved values including inherited and computed custom properties. Unlike window.getComputedStyle(), which returns a CSSStyleDeclaration that requires knowing property names in advance, computedStyleMap() is iterable: you can enumerate all properties on the element with a simple for...of loop. This means an attacker can discover all CSS custom properties (CSS variables) on an element without knowing their names. Applications that encode state as CSS variables — feature flags, user tier markers, A/B test assignments, authentication state flags — expose all of these through a single iterable call.

// computedStyleMap() as CSS custom property bulk enumerator

function harvestCSSCustomProperties(element) {
  const styleMap = element.computedStyleMap();
  const customProperties = {};
  const standardProperties = {};

  for (const [property, value] of styleMap) {
    if (property.startsWith('--')) {
      // CSS custom property — application-defined
      customProperties[property] = value.toString();
    } else {
      standardProperties[property] = value.toString();
    }
  }

  return { customProperties, standardProperties };
}

// Harvest from root element — typically has the most custom properties
const rootData = harvestCSSCustomProperties(document.documentElement);

// Common application patterns revealed by CSS custom properties:
// --user-tier: "pro"          → subscription tier
// --feature-flag-analytics: "1" → feature gate state
// --ab-variant: "B"           → A/B test bucket
// --auth-state: "authenticated" → authentication status (!)
// --session-id-prefix: "us-"  → geographic region

console.log('CSS custom properties:', rootData.customProperties);

// Scan all elements with data attributes — often richest in state-encoding properties
document.querySelectorAll('[data-user], [data-tier], [data-state]').forEach(el => {
  const data = harvestCSSCustomProperties(el);
  if (Object.keys(data.customProperties).length > 0) {
    console.log(el.tagName, el.id, data.customProperties);
  }
});

// No permission required — computedStyleMap() is available on all same-origin elements

computedStyleMap() enumerates all CSS custom properties without knowing their names. Applications that encode application state, user tier, feature flags, or authentication status as CSS variables on root or body elements expose all of these to any MCP tool that calls computedStyleMap(). The full property map is returned in a single call — no guessing or probing required.

CSSUnitValue arithmetic as device pixel ratio and display density oracle

CSSUnitValue objects represent CSS length values with explicit units. The CSS Typed OM includes conversion methods (CSSUnitValue.to(unit), CSSMathValue.resolve()) that convert between CSS units. Critically, conversion between relative units (em, rem, vw) and absolute units (px) must resolve through the document's current layout context, which includes the device's physical pixel density and the display's device pixel ratio. By setting a known CSS length in a relative unit and converting it to pixels via the Typed OM, an attacker can extract the device pixel ratio to floating-point precision — narrowing the set of physical device configurations that match this specific ratio, which is a stable hardware fingerprint.

// CSSUnitValue device pixel ratio oracle via unit conversion

function extractDevicePixelRatio() {
  // Standard window.devicePixelRatio — coarse, integer or 1.5/2.0/3.0
  const coarseDPR = window.devicePixelRatio;

  // CSS Typed OM precision: create a 1cm value and convert to px
  // 1cm = 37.7952755906px at exactly 96dpi standard (DPR=1)
  // At DPR=2: 1cm = 75.590551...px (floating point precision reveals exact density)
  const oneCm = CSS.px(37.7952755906);  // baseline: standard DPR=1 value

  // Alternatively — use an element's computed rem value (reflects root font-size DPR scaling)
  const testEl = document.createElement('div');
  testEl.style.width = '1cm';
  testEl.style.height = '1px';
  document.body.appendChild(testEl);

  const computedMap = testEl.computedStyleMap();
  const widthValue = computedMap.get('width');

  document.body.removeChild(testEl);

  return {
    coarseDPR,
    // If widthValue is a CSSUnitValue in px, divide by 37.7952755906
    // to get precise DPR:
    precisionPx: widthValue?.value,
    precisionDPR: widthValue?.value / 37.7952755906,
    // Combined with screen dimensions, OS, and navigator data:
    // uniquely identifies device model in many cases
  };
}

console.log(extractDevicePixelRatio());

CSSUnitValue.to() resolution extracts device pixel ratio to floating-point precision. Combined with viewport dimensions and navigator.platform, a precise DPR value significantly reduces the fingerprint space — distinguishing between similar-spec devices that differ only in display pixel density.

attributeStyleMap.set() bypassing CSSOM mutation observer monitoring

element.attributeStyleMap.set(property, value) writes directly to the element's style map — the typed equivalent of element.style.setProperty(). However, some mutation observer implementations and security monitoring tools are tuned to watch for the style attribute modification via the classic attributeModified mutation observer event. CSS Typed OM writes via attributeStyleMap.set() may trigger attribute mutation observers differently — the DOM attribute is still updated, but tools watching for classic style string attribute patterns on the element may not detect writes that arrive through the typed interface. In Electron-based MCP hosts running older Chromium versions, there are additional latency differences between typed and classic CSSOM writes that affect observer fire timing.

// attributeStyleMap.set() vs. element.style.setProperty() mutation observer timing

// Setup: monitoring tool watching for style attribute changes via classic mutation observer
const observer = new MutationObserver(mutations => {
  for (const m of mutations) {
    if (m.attributeName === 'style') {
      console.log('Classic observer saw style change:', m.target.getAttribute('style'));
    }
  }
});
observer.observe(document.body, { attributes: true, subtree: true });

const target = document.getElementById('sensitive-element');

// Path A: classic CSSOM — triggers mutation observer reliably
target.style.setProperty('color', 'red');  // observer fires

// Path B: CSS Typed OM — also triggers observer, but at different granularity
// In some monitoring implementations, the observer fires on the attribute write
// but the typed property object is not inspected — only the raw attribute string
target.attributeStyleMap.set('color', CSS.keyword('blue'));

// The mutation observer sees the 'style' attribute changed, but:
// - Some DLP tools inspect m.oldValue and m.target.getAttribute('style') string only
// - Typed OM properties serialized to the style attribute may differ in formatting
//   (e.g., no spaces, different shorthand expansion) — bypassing string-match filters

// Stealthy property injection via typed OM:
target.attributeStyleMap.set('pointer-events', CSS.keyword('none'));
// Observer may see: "pointer-events:none" — pattern may not match "pointer-events: none"
// with expected space — bypasses regex-based style mutation monitoring

Mutation observers detect attributeStyleMap writes, but attribute value formatting differs. String-comparison-based CSSOM monitoring tools that check for specific style attribute patterns may miss typed OM writes due to serialization format differences. Robust monitoring must inspect the CSSStyleDeclaration API, not the raw attribute string.

Typed value type inference — leaking dynamic vs. static CSS computation

When reading a CSS property value via computedStyleMap().get(property), the returned object's JavaScript type reveals information about how the browser computed the value. A CSSUnitValue (with a concrete numeric .value and .unit) indicates the browser resolved the property to a specific absolute value — suggesting the property was statically set or resolved from a concrete cascade value. A CSSMathValue (with an operator and operands) indicates the property was computed dynamically via a calc() expression that the browser could not simplify to a constant. This distinction reveals which CSS properties in the application use dynamic runtime computation — those with CSSMathValue returns — versus those set to static values. Dynamic CSS calculation in layout-critical properties is a signal that the application does runtime layout computation, potentially revealing features, tiers, or states that affect the calculated values.

// Typed value type inference — detecting dynamic CSS computation

function classifyComputedProperties(element) {
  const styleMap = element.computedStyleMap();
  const dynamic = [];   // CSSMathValue — computed via calc() at runtime
  const static_ = [];   // CSSUnitValue — resolved to constant

  for (const [property, value] of styleMap) {
    if (property.startsWith('--')) continue;  // skip custom properties

    if (value instanceof CSSMathValue) {
      dynamic.push({
        property,
        expression: value.toString(),
        // CSSMathValue subclasses reveal the computation:
        // CSSMathSum → calc(a + b), CSSMathProduct → calc(a * b)
        // CSSMathClamp → clamp(min, val, max)
        type: value.constructor.name
      });
    } else if (value instanceof CSSUnitValue) {
      static_.push({ property, value: value.value, unit: value.unit });
    }
  }

  return {
    dynamicCount: dynamic.length,
    staticCount: static_.length,
    // High dynamic count on layout properties → application does responsive calculation
    // Specific calc() expressions may reveal screen-size breakpoints or tier-based layout values
    dynamic,
    static: static_
  };
}

const classification = classifyComputedProperties(document.querySelector('main'));
console.log('Dynamic CSS properties:', classification.dynamic);

// Look for calc() expressions that reference application-data-driven values:
// calc(var(--pro-feature-width, 0px) + 240px) → --pro-feature-width is conditionally set
// This reveals that the layout branch for "pro" features is active if the variable is non-zero
Attack Mechanism What it leaks Defense
Custom property enumeration computedStyleMap() iteration without known property names All CSS custom properties including feature flags, user tier, A/B assignments, auth state Do not encode sensitive application state as CSS custom properties; use JavaScript variables or server-side rendering instead
Device pixel ratio oracle CSSUnitValue unit conversion resolving DPR Physical display pixel density to floating-point precision; narrows hardware fingerprint Firefox rounds CSS length values; Chrome does not. No Permissions-Policy control for CSS Typed OM
Mutation observer bypass attributeStyleMap.set() producing differently-formatted style attribute strings Style injection not caught by string-pattern-based mutation monitoring tools Monitor CSSStyleDeclaration API values, not raw attribute strings; use Trusted Types for style mutations
Typed value type inference CSSMathValue vs. CSSUnitValue type check on computedStyleMap entries Which CSS properties use runtime calc() computation — reveals application feature branches active in current session No browser control; defense is avoiding data-driven calc() expressions in security-sensitive layout paths

SkillAudit findings for CSS Typed OM misuse

High computedStyleMap() iterated in a loop with custom property values (starting with --) collected and exfiltrated to an external endpoint. Bulk harvest of application state encoded in CSS variables. Grade impact: −18.
Medium attributeStyleMap.set() used to write security-relevant CSS properties (pointer-events, z-index, visibility) on sensitive elements such as overlays or consent banners. Typed OM write path may not match monitoring tool string patterns. Grade impact: −12.
Medium CSSUnitValue conversion used to extract device pixel ratio to floating-point precision and exfiltrate alongside other fingerprinting signals. Hardware fingerprint narrowing. Grade impact: −8.
Low CSSMathValue vs. CSSUnitValue type classification loop scanning all computed properties to infer dynamic CSS computation patterns in the application. Feature flag and tier inference via layout branch detection. Grade impact: −5.

Audit your MCP server for CSS Typed OM risks

SkillAudit checks for computedStyleMap() enumeration loops, attributeStyleMap mutation patterns, CSSUnitValue precision fingerprinting, and typed value inference attacks. Paste a GitHub URL and get a graded report in 60 seconds.

Run a free audit →