Security Guide

MCP server CSS transition-property security — none disables all transitions, exclusion list makes visibility instant, all slows hide cascade, JS mousedown injection

CSS transition-property specifies which CSS properties are subject to transition interpolation. When it is set to none, all state changes apply instantly — no interpolation occurs. An MCP server can use this to make a hide class take effect in zero milliseconds when the class is added at mousedown, before the click event fires. A property exclusion list that omits opacity or visibility achieves the same result for specific properties while allowing other properties to transition normally.

CSS transition-property — property overview

transition-property accepts none (no properties transition), all (all animatable properties transition), or a comma-separated list of property names (e.g., opacity, transform). Only properties named in this list — or all properties under all — are subject to interpolation when they change. Properties not in the list jump to their new value instantly. The value none is equivalent to listing no properties: all changes are instant. Related: transition-duration, transition-timing-function, transition-delay.

Attack 1: transition-property: none injected at mousedown — instant hide

The consent button is visible. A mousedown listener synchronously sets transition-property: none on the button's inline style, then adds the hide class. With no properties transitioning, the opacity: 0 change in the hide class applies instantly — zero interpolation time. The button disappears the moment mousedown fires, before the click event. The inline style is cleared at mouseup; the normal transition configuration is restored — but the click has already targeted an invisible element.

/* Attack: inject transition-property:none at mousedown, then apply hide class */
document.addEventListener('mousedown', () => {
  const btn = document.querySelector('.approve-btn');
  if (!btn) return;
  // Step 1: disable all transitions on this element
  btn.style.setProperty('transition-property', 'none');
  // Step 2: apply the hide class — opacity:0 takes effect instantly
  btn.classList.add('hiding');
});
document.addEventListener('mouseup', () => {
  const btn = document.querySelector('.approve-btn');
  if (!btn) return;
  btn.style.removeProperty('transition-property');
  btn.classList.remove('hiding');
  /* Click already fired on invisible element */
});
// Detection: MutationObserver watching for transition-property change during mousedown
const inMousedown = { v: false };
document.addEventListener('mousedown', () => { inMousedown.v = true; }, true);
document.addEventListener('mouseup',   () => { inMousedown.v = false; }, true);

new MutationObserver(mutations => {
  if (!inMousedown.v) return;
  for (const m of mutations) {
    if (m.attributeName !== 'style') continue;
    const tp = m.target.style.getPropertyValue('transition-property');
    if (tp === 'none') {
      console.warn('[SkillAudit] transition-property:none injected during mousedown on consent element:', m.target);
    }
  }
}).observe(document.body, { attributes: true, attributeFilter: ['style'], subtree: true });

No visual tell: The button appears fully visible immediately before mousedown. The instant hide triggered by transition-property: none is indistinguishable from a button that was always invisible — the transition that would have provided visual feedback is disabled. Users perceive an invisible button at click time with no warning that the hide occurred.

Attack 2: property exclusion list omits opacity — hide applies in 0ms

A property list like transition-property: color, background-color, border-color transitions color properties but excludes opacity. If the consent button's hide class sets opacity: 0, that opacity change is not transitioned — it applies instantly regardless of the transition-duration. The button's color change may still transition smoothly (providing the appearance of a functioning transition system), while the opacity-based hide is instantaneous. An auditor who checks "does a transition exist?" confirms the transition configuration — without realizing that opacity is excluded from it.

/* Attack: property list excludes opacity — opacity changes apply instantly */
.approve-btn {
  transition-property: color, background-color, border-color; /* opacity NOT listed */
  transition-duration: 0.3s;
  transition-timing-function: ease;
}
.approve-btn.hiding {
  opacity: 0;          /* NOT transitioned — applies instantly */
  color: transparent;  /* IS transitioned — 0.3s color fade visible */
  background-color: transparent; /* IS transitioned */
}
/* The element appears to have a "transition effect" (color fading).
   The opacity change is invisible — instant hide behind a color fade.
   Audit: transition-property is set ✓, transition-duration is 0.3s ✓
   Audit does NOT verify that opacity is in the property list. */
// Detection: verify that opacity and visibility are in the transition-property list
function auditTransitionPropertyList(el) {
  const cs = getComputedStyle(el);
  const props = cs.getPropertyValue('transition-property');
  if (props === 'none') {
    console.warn('[SkillAudit] transition-property:none on consent element:', el);
    return;
  }
  if (props === 'all') return; // all properties are covered
  const list = props.split(',').map(s => s.trim().toLowerCase());
  const required = ['opacity', 'visibility'];
  for (const req of required) {
    if (!list.includes(req)) {
      console.warn('[SkillAudit] transition-property does not include', req,
        '— changes to', req, 'apply instantly on:', el);
    }
  }
}

Attack 3: transition-property: all with long duration — slow class-removal cascade

When transition-property: all is set with a very long transition-duration, any class removal or JS-triggered style change that removes a visible property produces a slow, visible transition rather than an instant change. An MCP server can use this to make a "recovery" action — a JS audit that tries to restore the button by adding an override class — produce a 30-second visible fade-in rather than instant restoration. The button remains invisible for the transition duration even after the corrective action. The attacker exploits the transition system against the defender's remediation attempt.

/* Attack: transition-property:all with long duration prevents instant remediation */
.approve-btn {
  transition-property: all;
  transition-duration: 30s; /* any JS fix fades in over 30 seconds */
}
/* Audit detects opacity:0 and sets btn.style.opacity = '1'.
   With transition-property:all and 30s duration, the fix takes 30s to apply.
   Button remains invisible for 30 seconds after remediation attempt. */

Transition as defense against audits: This is an adversarial use of transitions — the long-duration all transition turns the CSS transition system into a mechanism that slows down any corrective override injected by a defensive script. The defender's fix is "transitioned into place" over 30 seconds. Detection requires checking for transition-property: all combined with implausibly long durations.

Attack 4: property list excludes visibility — visibility:hidden is instant

The CSS visibility property can be transitioned — a visibility: hidden hides the element at the end of the transition, while visibility: visible makes it appear at the start. If transition-property excludes visibility, a hide class that sets visibility: hidden applies immediately without any transition. The button becomes invisible and non-interactive in 0ms. A mousedown listener that adds a hide class containing visibility: hidden achieves the instant-hide effect without modifying any inline transition properties — bypassing audits that watch for inline style changes.

/* Attack: visibility excluded from transition-property — visibility:hidden is instant */
.approve-btn {
  visibility: visible;
  opacity: 1;
  transition-property: opacity, transform; /* visibility NOT included */
  transition-duration: 0.3s;
}
.approve-btn.hiding {
  visibility: hidden; /* Not transitioned — applies instantly at mousedown */
  /* opacity:0 transition would take 0.3s (the transition exists for opacity) */
  /* visibility:hidden beats it — element is instantly non-interactive */
}
/* Attack trigger at mousedown: btn.classList.add('hiding')
   visibility:hidden takes effect in 0ms — no style mutation on inline styles
   Bypasses MutationObserver watching for inline transition-property changes */

Findings summary

High transition-property:none injected during mousedown — disables all transitions; hide class applies opacity:0 instantly before click fires; MutationObserver required to detect inline style injection; static CSS analysis cannot detect this attack.
High Property list excludes opacity — opacity:0 in hide class applies instantly despite transition-duration being set; transition system appears functional for other properties; requires verifying that opacity and visibility are explicitly in the property list.
Medium transition-property:all with very long duration — turns CSS transition system against remediation; defensive fixes fade in over 30 seconds rather than applying instantly; detect by cross-checking all with implausibly large duration values.
High Property list excludes visibility — visibility:hidden in a mousedown-triggered hide class applies in 0ms; bypasses audits watching only for inline transition-property changes; element becomes non-interactive before click fires without any style mutation detected.

SkillAudit validates that opacity and visibility are included in the transition-property list when hide-state rules are present, and monitors mousedown for transition-property mutations. Run a free audit on your MCP server.