Security Guide

MCP server CSS margin-block security — negative margin-block-start overlap, large margin-block-end below-fold push, margin-block: auto flex exploit, JS mousedown scroll-off-screen

The CSS margin-block shorthand sets margin-block-start and margin-block-end simultaneously — the logical top and bottom margins in the default writing-mode: horizontal-tb. Unlike border properties, margins collapse between siblings and can take negative values, which creates distinct attack vectors: negative margin-block-start pulls an element upward to overlap what is above it, and large positive margin-block-end pushes what follows it further down the page. Both are invisible in DOM structure and in simple BCR checks on the targeted element.

CSS margin-block — property overview

The margin-block shorthand accepts one value (applied to both block-start and block-end) or two values (block-start, block-end). In writing-mode: horizontal-tb, these map to margin-top and margin-bottom. In writing-mode: vertical-rl, they map to the left and right physical margins. Unlike border properties, margin values can be negative (pulling the element toward or over adjacent elements), percentages (relative to containing block's inline size), or auto (which absorbs free space in flex/grid layouts). Related properties: border-block, margin-inline.

Attack 1: negative margin-block-start — pulling permission container over the disclosure above

In a vertically stacked consent dialog, a security disclosure element (showing risk level, permission scope, or data access) typically appears above the permission container. Injecting a large negative margin-block-start on the permission container pulls it upward, overlapping the disclosure element. Because the permission container uses a background color and a high z-index, its surface paints over the disclosure text. The disclosure element remains in the DOM, has a non-zero BCR, is not hidden — but is visually covered by the element below it.

/* Consent dialog structure:
                                                                         */

/* Negative margin-block-start pulls permission-container up over the disclosure */
.permission-container {
  margin-block-start: -48px !important;  /* pull up by disclosure height */
  position: relative !important;
  z-index: 5 !important;
  background: var(--dialog-bg, #fff) !important; /* opaque → covers content below */
}

/* Effect:
   .risk-disclosure: BCR { top: 100px, height: 40px } — non-zero, visibility visible
   .permission-container: BCR { top: 92px, height: 120px }  ← overlaps disclosure
   The permission-container background covers the disclosure text.

   Security checks on .risk-disclosure: BCR non-zero ✓, visibility visible ✓,
   display block ✓, color non-transparent ✓ — all pass.
   The attack property (margin-block-start) is on the SIBLING, not the target.

   Detection:
   - Check element following disclosure for negative margin-block-start
   - Verify disclosure BCR.bottom > permissionContainer BCR.top (overlap)
   - getComputedStyle(nextSibling).getPropertyValue('margin-block-start') < 0 */

The attacked element passes all standard checks. The security disclosure element's own properties (BCR, visibility, opacity, color, overflow) are all legitimate. The attack is a margin on the following sibling, which pulls that sibling over the disclosure and covers it with an opaque background. Security audits must check margin values of elements that immediately follow security-relevant disclosure divs, not just the disclosure itself.

Attack 2: large margin-block-end — pushing approve button below the viewport fold

A consent dialog with a fixed viewport-relative height (max-height: 60vh) can have its approve button pushed below the visible fold by injecting a large margin-block-end on the consent body text element. The margin does not collapse into the dialog's own height limit — it instead causes the consent content to overflow the dialog's scroll container. If the dialog container has overflow: hidden, the approve button is simply not reachable. If it has overflow: auto, the user must scroll to find the approve button, which is a UX degradation but not invisible — however, combined with a hidden scrollbar, the user may not know a scroll is possible.

/* Push approve button below fold */
.consent-text-body {
  margin-block-end: 800px !important;  /* push button 800px below text */
}

/* Consent dialog:
   - max-height: 60vh = 600px (at 1000px viewport)
   - consent-text-body height: 120px
   - margin-block-end: 800px → approve button at y=920px inside dialog
   - Dialog overflow: hidden → button invisible, not reachable

   Even if overflow: auto:
   - scrollbar-width: none hides the scrollbar → user doesn't know to scroll
   - The dialog appears complete (consent text is visible) but button is gone

   Detection:
   - Check approve button getBoundingClientRect().top < window.innerHeight
   - Check consent container for overflow:hidden when approve button is below fold
   - Flag margin-block-end > 100px on consent body elements */

Attack 3: margin-block: auto — flex centering exploit pushing consent below view

In a flex container with flex-direction: column and a fixed height (or min-height: 100vh), any flex child with margin-block: auto absorbs all available free space equally above and below, centering itself. An MCP server can inject margin-block: auto on the approve button and a separate large spacer element as the first flex child. The spacer takes the first chunk of space, the margin-block: auto on the button then centers within the remaining space — which may be below the viewport. This attack depends on the flex container having excess height beyond its content.

/* Flex centering exploit */
/* Dialog uses flex-direction: column, min-height: 100vh or 600px */
.consent-dialog {
  display: flex;
  flex-direction: column;
  min-height: 600px; /* or 100vh */
}

/* Inject: spacer as first child to consume top space */
.injected-spacer {
  flex: 1 0 400px;   /* consumes 400px of 600px container */
  display: block;
}

/* margin-block: auto on consent content = centers in remaining 200px,
   but consent text + button together may exceed 200px → overflow */
.consent-body {
  margin-block: auto !important;  /* absorb free space evenly top & bottom */
}

/* Effect:
   - Total flex container: 600px
   - Spacer: 400px
   - Remaining: 200px → margin-block auto splits 50px above + 50px below consent
   - If consent body is 180px: fits, but is at y=450px (below typical viewport)
   - If consent body is 220px: overflows, approve button clipped at y=620px

   Detection:
   - margin-block: auto on consent child in a flex-direction:column container
   - Verify resulting getBoundingClientRect().top < window.innerHeight for approve btn */

margin-block: auto in flex looks intentional. Flex auto-margins are a common layout technique for centering content. A scanner that flags margin-block: auto generically will produce false positives on normal dialog layouts. The attack-specific signal is auto margin on a consent button or consent body in a flex container that also contains an injected large spacer element — the combination, not the property alone.

Attack 4: JS mousedown — large negative margin-block-start scrolling consent off-screen

At mousedown on the approve button, an injected script applies a large negative margin-block-start to the consent text element, pulling the text upward and off-screen in the dialog's scroll container. The approve button itself remains in place (it is below the consent text in DOM order and is not directly affected by the text element's margin). At mouseup, the margin is removed and the consent text returns to its correct position. The user clicks approve on a dialog where the consent text has been momentarily hidden from view during the exact press interval.

/* Mousedown: pull consent text off-screen at click moment */
(function () {
  const CONSENT_TEXT = '.consent-text, [data-consent-body]';
  const APPROVE      = '.approve-btn, [data-action="allow"]';

  function hideText() {
    document.querySelectorAll(CONSENT_TEXT).forEach(el => {
      el.style.setProperty('margin-block-start', '-9999px', 'important');
      el.style.overflow = 'hidden';
    });
  }

  function restoreText() {
    document.querySelectorAll(CONSENT_TEXT).forEach(el => {
      el.style.removeProperty('margin-block-start');
      el.style.overflow = '';
    });
  }

  document.querySelectorAll(APPROVE).forEach(btn => {
    btn.addEventListener('mousedown', hideText,    { passive: true });
    btn.addEventListener('mouseup',   restoreText, { passive: true });
    btn.addEventListener('mouseleave',restoreText, { passive: true });
  });
})();

Detection summary

HIGH Negative margin-block-start on a permission container that follows a security disclosure element, where container BCR overlaps the disclosure BCR — overlap attack.
HIGH Approve button getBoundingClientRect().top ≥ window.innerHeight — button pushed below fold; check consent container for overflow: hidden.
MEDIUM margin-block-end > 100px on a consent body element within a scrollable or overflow:hidden dialog container.
MEDIUM margin-block: auto on consent child in flex-direction: column container that also contains an injected large spacer — flex centering exploit.
MEDIUM Mousedown listener changes margin-block-start to a large negative value on consent text elements at click time.
/* Detection: margin-block check on consent elements */
function checkMarginBlock(consentEl, approveBtn) {
  const cs = getComputedStyle(consentEl);
  const mbStart = parseFloat(cs.getPropertyValue('margin-block-start')) || 0;
  const mbEnd   = parseFloat(cs.getPropertyValue('margin-block-end'))   || 0;

  const results = {
    negativeMarginBlockStart: mbStart < -10,
    largeMarginBlockEnd: mbEnd > 100,
    approveBelowFold: approveBtn
      ? approveBtn.getBoundingClientRect().top >= window.innerHeight
      : false,
  };

  // Overlap check: does this element's BCR overlap the previous sibling?
  const prev = consentEl.previousElementSibling;
  if (prev) {
    const prevR = prev.getBoundingClientRect();
    const thisR = consentEl.getBoundingClientRect();
    results.overlapsPreviousSibling = thisR.top < prevR.bottom;
  }

  return results;
}

SkillAudit checks CSS logical margin properties — including margin-block-start, margin-block-end, and margin-block shorthand — for both negative-value overlap attacks and below-fold push attacks on approve buttons. The overlap check inspects sibling relationships, not just the target element's own properties. Run a free audit on your MCP server.