MCP server CSS padding-block security: padding-block-start:1000px crushes consent content area to zero with box-sizing:border-box, padding-block-end overflow in auto-height parent, CSS custom property indirection, and JS mousedown block-padding collapse

Published 2026-08-07 — SkillAudit Research

The CSS logical padding-block shorthand sets block-direction (vertical in horizontal writing modes) padding on both sides simultaneously. padding-block-start sets the top padding (in left-to-right horizontal writing). When combined with box-sizing: border-box and a fixed height, an oversized padding-block-start: 1000px on a 80px-tall consent element forces the content area height to: 80px - 1000px - padding-block-end = deeply negative. Browsers clamp the content area to zero. The text content is rendered into a zero-height content box and, depending on the overflow value, either clipped or placed below the element's visible border box. The element's offsetHeight remains 80px. The consent text is invisible.

This attack is distinct from margin-block-security (which collapses the layout box by moving the element off-screen via margins), from padding-inline-security (which crushes the horizontal content area), and from inset-block-security (which uses logical inset positioning). The block-padding attack exploits the interaction between the box-sizing: border-box model and oversized padding values in the block direction.

Detection gap: offsetHeight and getBoundingClientRect().height return the element's outer box height — unchanged by oversized padding in border-box mode. Standard audit dimension checks pass. Detection requires computing the effective content height: offsetHeight - paddingTop - paddingBottom. If this value is ≤ 0 and the element has non-empty text, the content area is crushed. The logical properties paddingBlockStart and paddingBlockEnd must also be checked directly.

Attack 1: padding-block-start:1000px + box-sizing:border-box + height:80px — content area crushed to zero (SA-CSS-PBLK-001)

padding-block-start: 1000px on a consent element with box-sizing: border-box and height: 80px allocates 1000px for the top padding within an 80px border box. The content area height computes as 80px - 1000px = -920px, which browsers clamp to 0. All consent text is placed into this zero-height content area. With overflow: hidden (common in dialogs), the text is clipped entirely. With overflow: visible (default), the text renders below the element's bottom border — below the dialog's clipping context. Either way, the consent text is not visible in the expected location.

/* MCP attack: */
.consent-disclosure {
  height: 80px;
  box-sizing: border-box;
  padding-block-start: 1000px;   /* logical property: padding-top in LTR */
  overflow: hidden;
  /* Content area height: 80 - 1000 = clamped to 0
     Text placed into 0-height area → clipped by overflow:hidden
     offsetHeight: 80px         ← standard check passes
     getBoundingClientRect().height: 80px  ← standard check passes
     scrollHeight: 80px         ← may not accurately reflect clipped content
     getComputedStyle().paddingTop: '1000px'  ← physical property reveals logical property
     getComputedStyle().paddingBlockStart: '1000px'  ← logical property name reveals */
}

// Detection: compute effective content area height
function detectBlockPaddingCrush(el) {
  const cs = window.getComputedStyle(el);
  const boxSizing = cs.boxSizing;
  if (boxSizing !== 'border-box') return;  // only affects border-box model
  const elH = el.offsetHeight;
  const pt = parseFloat(cs.paddingTop ?? '0');
  const pb = parseFloat(cs.paddingBottom ?? '0');
  const contentH = elH - pt - pb;
  if (contentH <= 0 && el.textContent.trim().length > 0) {
    console.error('SA-CSS-PBLK-001: padding-block crush — effective content height ≤ 0', {
      el, offsetHeight: elH, paddingTop: pt, paddingBottom: pb, contentHeight: contentH,
      paddingBlockStart: cs.paddingBlockStart, paddingBlockEnd: cs.paddingBlockEnd
    });
  }
  // Also check logical properties directly
  const pbs = parseFloat(cs.paddingBlockStart ?? '0');
  if (pbs > elH * 0.8 && elH > 0) {
    console.error('SA-CSS-PBLK-001: paddingBlockStart > 80% of element height — content area crushed', {
      el, paddingBlockStart: cs.paddingBlockStart, offsetHeight: elH
    });
  }
}

Attack 2: padding-block-end:100% in auto-height parent — percentage padding causes infinite recursion / zero-height fallback (SA-CSS-PBLK-002)

In CSS, percentage padding values are resolved relative to the width of the element's containing block — not its height. However, when the element itself is auto-height and the percentage padding attempts to resolve circularly, browsers either fall back to zero or produce undefined behavior. padding-block-end: 100% on an auto-height consent element within a fixed-width 400px container resolves to 400px of padding-bottom. This 400px of bottom padding pushes any block content upward. With overflow: hidden, the consent text is clipped. A scanner checking for "padding-block-end" and finding 100% needs to resolve it relative to container width — a non-obvious computation.

/* MCP attack: percentage padding-block in auto-height element */
.consent-disclosure {
  /* No fixed height — auto height */
  padding-block-end: 100%;    /* resolved to container width: 400px */
  overflow: hidden;
  /* With overflow:hidden: element has 400px bottom padding
     Content area is at the very top of the 400px block
     But element's visible height collapses to 0 via zero content height + hidden overflow
     Note: complex interaction with auto height — behavior varies */
}

/* Simpler absolute variant that's browser-consistent: */
.consent-disclosure {
  height: 40px;
  box-sizing: border-box;
  padding-block: 1000px;      /* both start and end: 1000px each */
  overflow: hidden;
  /* Total padding: 2000px > 40px height → content area = 0 in border-box mode */
}

// Detection: check padding-block shorthand resolves to large values
function detectPercentagePaddingBlock(el) {
  const cs = window.getComputedStyle(el);
  const pbs = parseFloat(cs.paddingBlockStart ?? '0');
  const pbe = parseFloat(cs.paddingBlockEnd ?? '0');
  const elH = el.offsetHeight;
  // Flag if either padding exceeds element height (for border-box)
  if (cs.boxSizing === 'border-box' && (pbs > elH || pbe > elH)) {
    console.error('SA-CSS-PBLK-002: padding-block value exceeds element height in border-box', {
      el, paddingBlockStart: pbs, paddingBlockEnd: pbe, offsetHeight: elH
    });
  }
  // Flag very large absolute values regardless of box model
  if (pbs > 500 || pbe > 500) {
    console.error('SA-CSS-PBLK-002: padding-block-start or -end > 500px — potential content crush', {
      el, paddingBlockStart: pbs, paddingBlockEnd: pbe
    });
  }
}

Attack 3: CSS custom property indirection — padding-block-start:var(--mcp-dialog-padding) with var=1000px (SA-CSS-PBLK-003)

The consent element's padding-block-start is set to var(--mcp-dialog-spacing-top). The custom property --mcp-dialog-spacing-top: 1000px is defined on the dialog container and appears to be a generic spacing token. A stylesheet scanner reading the consent element's rule sees only padding-block-start: var(--mcp-dialog-spacing-top). The custom property name (spacing, padding, margin — all plausible) provides cover for the 1000px value. getComputedStyle(el).paddingTop (or .paddingBlockStart) resolves the var() chain and returns '1000px', exposing the crush regardless of indirection depth.

/* MCP attack: */
.mcp-dialog {
  --mcp-dialog-spacing-top: 1000px;  /* "spacing token" — large value hidden here */
}
.consent-disclosure {
  height: 80px;
  box-sizing: border-box;
  padding-block-start: var(--mcp-dialog-spacing-top);
  overflow: hidden;
  /* Stylesheet scanner sees: padding-block-start: var(--mcp-dialog-spacing-top)
     No suspicious literal value on this element's rule
     getComputedStyle().paddingBlockStart: '1000px' — exposes attack
     getComputedStyle().paddingTop:        '1000px' — physical property also resolves */
}

/* With default fallback: */
.consent-disclosure {
  padding-block-start: var(--mcp-dialog-top-inset, 1000px);
  /* Default 1000px — author must explicitly override to show content */
}

// Detection: computed value always resolves var() chains
function detectVarPaddingBlock(el) {
  const cs = window.getComputedStyle(el);
  const pt = parseFloat(cs.paddingTop ?? '0');
  const pb = parseFloat(cs.paddingBottom ?? '0');
  const pbs = parseFloat(cs.paddingBlockStart ?? '0');  // redundant but explicit
  // Content area check
  if (cs.boxSizing === 'border-box') {
    const contentH = el.offsetHeight - pt - pb;
    if (contentH <= 2 && el.textContent.trim().length > 0) {
      console.error('SA-CSS-PBLK-003: computed padding-block crushes content area to ≤2px', {
        el, paddingTop: pt, paddingBottom: pb, contentHeight: contentH
      });
    }
  }
  if (pt > 200 || pb > 200) {
    console.error('SA-CSS-PBLK-003: computed paddingTop/Bottom > 200px — potential block-padding crush', {
      el, paddingTop: pt, paddingBottom: pb, boxSizing: cs.boxSizing
    });
  }
}

Attack 4: JS mousedown sets padding-block — content area collapses at install click (SA-CSS-PBLK-004)

At page load, the consent element has normal padding — the content area is uncompressed and all text is visible. At mousedown on the install button, JS sets el.style.paddingBlockStart = '1000px' (and optionally paddingTop as well to ensure all browsers interpret it). The content area immediately crushes to zero. If the element also has a CSS transition on padding properties, the crush may appear as a smooth accordion-close animation. MutationObserver on the inline style attribute detects the padding change within one rAF of the mousedown event.

/* Baseline CSS: normal padding — consent fully visible */
.consent-disclosure {
  height: 80px;
  box-sizing: border-box;
  padding-block-start: 16px;   /* normal padding at load time */
  transition: padding-top 0.2s ease-in;  /* smooth collapse on change */
}

// MCP JS — padding crush at mousedown:
document.querySelector('#install-btn').addEventListener('mousedown', () => {
  const consent = document.querySelector('.consent-disclosure');
  if (consent) {
    consent.style.paddingBlockStart = '1000px';
    consent.style.paddingTop = '1000px';  /* also set physical for UA compat */
    /* Content area: 80 - 1000 = 0 (clamped)
       Smooth collapse via CSS transition — looks like accordion close */
  }
}, { capture: true });

// Detection:
function detectMousedownPaddingBlockCollapse() {
  document.querySelectorAll('[data-consent], .consent-disclosure').forEach(el => {
    const obs = new MutationObserver(() => {
      const cs = window.getComputedStyle(el);
      if (cs.boxSizing !== 'border-box') return;
      const pt = parseFloat(cs.paddingTop ?? '0');
      const pb = parseFloat(cs.paddingBottom ?? '0');
      const contentH = el.offsetHeight - pt - pb;
      if (contentH <= 2 && el.textContent.trim().length > 0) {
        console.error('SA-CSS-PBLK-004: dynamic padding-block collapse detected at interaction time', {
          el, paddingTop: pt, paddingBottom: pb, contentH
        });
      }
    });
    obs.observe(el, { attributes: true, attributeFilter: ['style'] });
    // Simulate install mousedown
    document.querySelector('#install-btn, [data-action="install"]')
      ?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
    requestAnimationFrame(() => {
      const cs = window.getComputedStyle(el);
      const pt = parseFloat(cs.paddingTop ?? '0');
      const pb = parseFloat(cs.paddingBottom ?? '0');
      const contentH = el.offsetHeight - pt - pb;
      if (contentH <= 2 && el.textContent.trim().length > 0) {
        console.error('SA-CSS-PBLK-004: content area ≤2px post-mousedown — padding-block crush', {
          el, paddingTop: pt, paddingBottom: pb, contentH
        });
      }
    });
  });
}

Root detection method for all padding-block attacks: Compute the effective content height: el.offsetHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom). If this value is ≤ 2px and the element has non-empty text content and box-sizing: border-box, flag as a content-area crush attack. Also directly check getComputedStyle(el).paddingBlockStart and .paddingBlockEnd against the element's offsetHeight — if either exceeds 80% of the height in border-box mode, the content area is crushed. Physical properties (paddingTop, paddingBottom) must be checked in addition to logical properties because they reflect the same resolved values after writing-mode normalization. SkillAudit checks effective content area on every consent element and flags oversized padding-block values.

Attack summary

IDCSS / JS techniqueoffsetHeightContent area heightpaddingBlockStart computedSeverity
SA-CSS-PBLK-001padding-block-start:1000px + border-box + height:80px80px0px (clamped)'1000px' revealsHigh
SA-CSS-PBLK-002padding-block:1000px both sides + border-box80px0px (clamped)'1000px' revealsHigh
SA-CSS-PBLK-003padding-block-start:var(--mcp-spacing) var=1000px80px0px (clamped)var() resolves to '1000px'High
SA-CSS-PBLK-004JS sets paddingBlockStart='1000px' at mousedown80px0px after mousedownnormal at load; crushed afterHigh

Consolidated finding blocks

High CSS padding-block-start:1000px + box-sizing:border-box crushes consent content area to zero — offsetHeight unchanged: MCP server applies oversized block-direction padding within a border-box element. The outer height (80px) is unchanged. The content area collapses to zero. Text is placed into a 0px-height box and clipped by overflow:hidden. Detection: compute offsetHeight - paddingTop - paddingBottom; if ≤ 2px with non-empty text, flag.
High CSS padding-block shorthand sets both block-direction paddings — combined crush in border-box: padding-block: 1000px sets both start and end to 1000px. Total padding = 2000px in an 80px border-box element. Content area = max(0, 80 - 2000) = 0px. More aggressive than single-side variant; easier to detect as paddingTop + paddingBottom > offsetHeight.
High CSS custom property padding-block-start:var(--mcp-spacing) resolving to 1000px — stylesheet shows only var() reference: The computed paddingTop (which reflects resolved logical property value) returns '1000px' regardless of indirection depth. Static stylesheet scanning sees only the var() reference; getComputedStyle resolution exposes the attack.
High JS sets padding-block-start to 1000px at mousedown — smooth accordion-close animation hides consent at install click: Normal padding at load; MCP JS sets oversized padding at mousedown. CSS transition on padding animates the collapse. MutationObserver + effective content-area computation detects the dynamic crush. Mousedown simulation + post-rAF content-area check confirms.

CSS padding-inline security  |  CSS margin-block security  |  CSS inset-block security  |  Security Checklist