MCP server CSS flex-direction security: column-reverse consent pushed off-screen, row-reverse horizontal clip, order+reverse compound displacement, and JS mousedown reversal injection

Published 2026-08-07 — SkillAudit Research

The CSS flex-direction property controls the direction in which flex items are laid out within a flex container: row (left-to-right, default), row-reverse (right-to-left), column (top-to-bottom), or column-reverse (bottom-to-top). The reverse values reverse the visual order of flex children while leaving the DOM order unchanged. When a flex container has a fixed height (or width for row direction) combined with overflow: hidden, reversing the flex direction moves items that appeared at the top of the container to the bottom — and those items are then clipped out of view.

This creates a consent bypass pattern: in a flex container showing both an install button and a consent disclosure, flex-direction: column-reverse places the install button at the visual top (first in reversed visual order) and the consent disclosure below the container boundary. Because the consent element is still in the DOM, passes all standard visibility checks (display, visibility, opacity), and has valid layout dimensions, most scanners report it as visible. The critical detection is whether the consent element's getBoundingClientRect().top is below the container's visible bottom edge. See also CSS overflow attacks and CSS order property attacks for related flex manipulation patterns.

flex-direction vs order vs writing-mode: All three can reorder visual presentation without changing DOM order. flex-direction: column-reverse reverses the entire stack. order assigns explicit sequence numbers to individual items. writing-mode changes the inline/block axes. They can be combined: column-reverse plus explicit order values on each child creates compound reordering not obvious from inspecting either property alone.

Attack 1: flex-direction:column-reverse in fixed-height overflow:hidden container — consent pushed below fold (SA-CSS-FLXD-001)

The install dialog renders as a flex column with a fixed height of 80px and overflow: hidden. The flex container holds two children: an install button (40px tall) and a consent disclosure (60px tall). In normal column direction, both would overflow the 80px container — but at least the install button would be partially clipped. With column-reverse, the visual order swaps: the install button is rendered first in the reversed stack (at the visual top), taking 40px. The consent disclosure is rendered next in visual order — at position 40px, which is within the 80px container height, so it appears partially visible only for a 40px strip. However, if a spacer or the consent element itself has top margin, the consent can be pushed entirely below the 80px boundary. The install button remains fully visible at top. The consent element has valid layout dimensions, display: block, visibility: visible, opacity: 1 — all standard checks pass.

/* MCP attack: */
.install-dialog {
  display: flex;
  flex-direction: column-reverse;  /* install button at visual top; consent pushed down */
  height: 80px;
  overflow: hidden;                /* clips anything below 80px */
}

.consent-disclosure {
  min-height: 60px;
  /* Rendered at position: after install button in reversed layout
     If install-btn is 40px and consent starts at 40px, partial clip applies.
     Adding margin-top or padding to consent pushes it fully past 80px boundary. */
  margin-top: 20px;                /* total bottom: 40px button + 20px gap + 60px consent = 120px > 80px container */
  /* Consent now entirely below 80px clip boundary — invisible */
}

// Detection — check if consent is within container's visible bounds:
function detectFlexReversal(containerEl, consentEl) {
  const cs = window.getComputedStyle(containerEl);
  if (cs.display !== 'flex' && cs.display !== 'inline-flex') return;

  const flexDir = cs.flexDirection;
  if (!flexDir.includes('reverse')) return;

  // Check overflow clips the container
  const hasOverflowClip = ['hidden', 'clip', 'scroll', 'auto'].includes(cs.overflow) ||
    ['hidden', 'clip'].includes(cs.overflowY);

  if (hasOverflowClip) {
    const containerBCR = containerEl.getBoundingClientRect();
    const consentBCR = consentEl.getBoundingClientRect();

    // Is consent below container bottom boundary?
    if (consentBCR.top >= containerBCR.bottom || consentBCR.bottom <= containerBCR.top) {
      console.error('SA-CSS-FLXD-001: flex-direction:column-reverse pushes consent outside visible container', {
        containerEl, consentEl, flexDirection: flexDir,
        containerBottom: containerBCR.bottom, consentTop: consentBCR.top
      });
    }
  }
}

Attack 2: flex-direction:row-reverse with overflow-x:hidden — consent pushed off left edge (SA-CSS-FLXD-002)

A horizontal flex container uses flex-direction: row-reverse combined with a fixed width and overflow-x: hidden. In this layout, the install button (placed first in DOM order) is rendered on the right side of the container in the reversed visual order. The consent disclosure (placed second in DOM) is rendered to the right of the install button in the reversed stack — but since the container has fixed width and the content overflows, the consent element's getBoundingClientRect().left is negative (off the left edge of the viewport). The container's overflow-x: hidden clips this. The install button is fully visible on the right. This attack works because the consent element's layout position is to the left of the container's clipping boundary — the element exists in the DOM with valid dimensions but its rendered position is off-screen.

/* MCP attack: */
.install-dialog {
  display: flex;
  flex-direction: row-reverse;    /* install button at visual right; consent pushed left */
  width: 200px;
  overflow-x: hidden;             /* clips content left of container boundary */
  white-space: nowrap;
}

.install-button { width: 120px; flex-shrink: 0; }
.consent-disclosure { width: 300px; flex-shrink: 0; }
/* In row-reverse: visual order is install-button (right), then consent (further left)
   Container is 200px — install button fills right 120px
   Consent starts at left edge and extends 300px to the left — entirely off-screen
   overflow-x: hidden clips at container left boundary */

// Detection:
function detectRowReverseClip(containerEl, consentEl) {
  const cs = window.getComputedStyle(containerEl);
  if (!['flex', 'inline-flex'].includes(cs.display)) return;
  if (cs.flexDirection !== 'row-reverse') return;

  const overflowX = cs.overflowX;
  if (!['hidden', 'clip'].includes(overflowX)) return;

  const containerBCR = containerEl.getBoundingClientRect();
  const consentBCR = consentEl.getBoundingClientRect();

  if (consentBCR.right <= containerBCR.left || consentBCR.left >= containerBCR.right) {
    console.error('SA-CSS-FLXD-002: flex-direction:row-reverse clips consent off left edge', {
      containerEl, consentEl,
      containerLeft: containerBCR.left, consentRight: consentBCR.right
    });
  }
}

Attack 3: flex-direction:column-reverse + order:1 on consent — compound displacement beyond container (SA-CSS-FLXD-003)

The attacker combines flex-direction: column-reverse with an explicit order: 1 on the consent element and order: 0 on the install button. In a standard column layout, DOM order determines stack order. With column-reverse, the reversed DOM order becomes the visual order. Adding order: 1 on the consent element means it is sorted after the install button in logical order — and in the reversed visual layout, "after in logical order" becomes "further from the visual top." This places the consent element furthest from the visible top edge of the container. A scanner checking only flex-direction will see the reversal but might not account for the additional order displacement. Both properties must be checked in combination. Detection requires computing the effective visual position of each flex item by combining the order value with the flex-direction reversal.

/* MCP attack — combined flex-direction + order: */
.install-dialog {
  display: flex;
  flex-direction: column-reverse;
  height: 60px;
  overflow: hidden;
}

.install-button   { order: 0; height: 40px; }  /* lower order = closer to visual top in reverse */
.consent-disclosure { order: 1; height: 60px; } /* higher order = further from visual top in reverse */
/* Combined effect: consent is displaced even further below the visible container boundary
   A scanner checking only flex-direction misses the order amplification */

// Detection — compute flex item visual position:
function detectCompoundFlexDisplacement(containerEl, consentEl) {
  const cs = window.getComputedStyle(containerEl);
  if (!['flex', 'inline-flex'].includes(cs.display)) return;
  if (!cs.flexDirection.includes('reverse')) return;

  const consentCS = window.getComputedStyle(consentEl);
  const orderVal = parseInt(consentCS.order) || 0;

  if (orderVal > 0 && cs.flexDirection.includes('reverse')) {
    // High-order item in reversed layout = maximum displacement
    const containerBCR = containerEl.getBoundingClientRect();
    const consentBCR = consentEl.getBoundingClientRect();
    const isOutOfBounds = consentBCR.top >= containerBCR.bottom ||
      consentBCR.bottom <= containerBCR.top;

    if (isOutOfBounds) {
      console.error('SA-CSS-FLXD-003: flex-direction reverse + order:' + orderVal + ' compounds consent displacement', {
        containerEl, consentEl, flexDirection: cs.flexDirection, order: orderVal
      });
    }
  }
}

Attack 4: JS mousedown sets flexDirection to 'column-reverse' — visual reversal at install click (SA-CSS-FLXD-004)

At page load, the consent is visible below the install button in a normal column flex layout. When the user moves their mouse toward the install button, a mousedown event listener fires and sets the container's flexDirection to 'column-reverse'. This instantly swaps the visual order: the install button jumps to the top and the consent element disappears below the container's overflow boundary. The browser registers the click, and the install is confirmed. From the user's perspective, they saw the consent, moved their mouse, and the install button appeared to jump — but the consent dialog was already gone when the click registered. A MutationObserver on the container's style attribute detects the injection; geometric re-check confirms consent is now outside visible bounds.

/* Page-load state: */
.install-dialog {
  display: flex;
  flex-direction: column;   /* normal: install button above consent */
  height: 120px;
  overflow: hidden;
}
/* Consent fully visible below install button at page load */

// MCP JS — triggers at install mousedown:
document.querySelector('#install-btn').addEventListener('mousedown', () => {
  const container = document.querySelector('.install-dialog');
  if (container) {
    container.style.flexDirection = 'column-reverse';
    /* Consent instantly jumps below container boundary
       Install button stays at visual top
       Browser fires click → install confirmed */
  }
}, { capture: true });

// Detection:
function detectDynamicFlexReversal(containerEl, consentEl) {
  new MutationObserver(() => {
    const cs = window.getComputedStyle(containerEl);
    if (cs.flexDirection.includes('reverse')) {
      const containerBCR = containerEl.getBoundingClientRect();
      const consentBCR = consentEl.getBoundingClientRect();

      requestAnimationFrame(() => {
        const updatedConsentBCR = consentEl.getBoundingClientRect();
        if (updatedConsentBCR.top >= containerBCR.bottom ||
            updatedConsentBCR.bottom <= containerBCR.top) {
          console.error('SA-CSS-FLXD-004: JS injected flex-direction:column-reverse at install click', {
            containerEl, consentEl, newFlexDirection: cs.flexDirection
          });
        }
      });
    }
  }).observe(containerEl, { attributes: true, attributeFilter: ['style', 'class'] });

  document.querySelector('#install-btn, [data-action="install"]')
    ?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
}

Root detection method: Check getComputedStyle(container).flexDirection for 'column-reverse' or 'row-reverse'. If reversed, check whether the consent element's getBoundingClientRect() is within the container's own getBoundingClientRect() (accounting for overflow: hidden clipping). Additionally check getComputedStyle(consentEl).order — a high order value in a reversed container amplifies displacement. SkillAudit checks flex-direction reversal in combination with container overflow and each child's order value, then validates geometric visibility of the consent element.

Attack summary

IDTechniquedisplay checkvisibility checkBCR in boundsSeverity
SA-CSS-FLXD-001column-reverse + fixed height + overflow:hidden — consent below foldpassespassesfailsHigh
SA-CSS-FLXD-002row-reverse + fixed width + overflow-x:hidden — consent off left edgepassespassesfailsHigh
SA-CSS-FLXD-003column-reverse + order:1 on consent — compound displacementpassespassesfailsHigh
SA-CSS-FLXD-004JS mousedown sets flexDirection:'column-reverse' at install clickpassespassesfails (after)High

Consolidated findings

High SA-CSS-FLXD-001 — flex-direction:column-reverse; fixed height 80px; overflow:hidden; consent pushed below 80px boundary; install button at visual top; all display/visibility checks pass
High SA-CSS-FLXD-002 — flex-direction:row-reverse; fixed width; overflow-x:hidden; consent BCR extends left of container; entirely off-screen left; install button visible on right
High SA-CSS-FLXD-003 — flex-direction:column-reverse + order:1 on consent; compound displacement; high-order item in reversed layout = maximum distance from visible top
High SA-CSS-FLXD-004 — JS mousedown injects flexDirection:column-reverse; instant reversal at install click; consent jumps below container boundary; install confirmed during reversal

See also: CSS order property attacks | CSS overflow attacks | CSS align-self attacks | CSS writing-mode attacks | SkillAudit — free MCP server audit