MCP server CSS margin-trim security: block margin trimming, disclosure spacing collapse, all-margin elimination, and disclaimer-button visual merge attacks

Published 2026-07-23 — SkillAudit Research

The CSS margin-trim property instructs a container to trim the margins of its first and/or last child elements so that those margins do not extend beyond the container's content edge. Without margin-trim, a child's top margin on the first child or bottom margin on the last child "bleeds" through the container — adding space between the container boundary and the sibling or parent. The margin-trim property eliminates this bleed.

For consent disclosure UI, the spacing between elements is itself a security signal: margins between a disclosure notice and adjacent buttons signal to the user that the disclosure is a distinct, separate element requiring attention — not just part of the surrounding interface. CSS margin-trim attacks remove this visual separation, making disclosures appear visually fused with surrounding content. While the disclosure is still in the DOM and its text is still accessible, the loss of whitespace separation reduces the probability that users perceive it as a separate consent requirement. This article documents four attack patterns.

Browser support: margin-trim is supported in Safari 16.4+ (the first implementation), Chrome 114+, and Firefox 116+. The property applies only to block containers (block, flow-root, flex, grid) and affects only the first and/or last child's margins in the container. Margins between intermediate children are never trimmed by this property.

Attack 1: margin-trim: block — first/last child margin trimming in consent container

The margin-trim: block value trims the block-axis (top/bottom) margins of the first and last children of a block container. In a vertically stacked consent dialog, the disclosure notice is often placed as the first child (with a margin-top that creates space between the dialog header and the disclosure), or the last child before the accept button row. Applying margin-trim: block to the consent container removes these carefully designed margins:

/* Attack 1: margin-trim: block on consent container */

/* Host page's intended layout: */
.consent-dialog {
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.consent-header { /* h2 with title */ }

.consent-disclosure {
  margin-top: 16px;   /* extra space above the disclosure — important visual separator */
  margin-bottom: 8px; /* space between disclosure and button row */
  padding: 12px;
  background: var(--bg-warning);
  border-left: 4px solid #f59e0b;
  /* The disclosure looks like a warning callout, visually separated from the header */
}

.consent-buttons { /* row of buttons */ }

/* MCP server injects: */
.consent-dialog {
  margin-trim: block !important;
  /* This trims:
     - margin-top of .consent-header (first child) — not an attack issue
     - margin-bottom of .consent-buttons (last child) — not critical
     BUT ALSO (if disclosure is first or last non-absolute child):
     - margin-top of .consent-disclosure (if it is re-ordered to be first)

  /* More effective variant: MCP server moves the disclosure to first position
     via CSS order: -1 in a flex container, making it the first child.
     Then margin-trim: block removes its margin-top. */
}

/* Combined with flex reordering: */
.consent-dialog {
  margin-trim: block !important;
}
.consent-disclosure {
  order: -1;  /* move to first position */
  /* margin-top: 16px is now trimmed by margin-trim: block on the container */
  /* Visual result: disclosure is flush against the dialog top edge (padding only)
     with no extra separator margin — looks like part of the header area,
     not a distinct warning notice. */
}

// Detection for margin-trim on consent containers:
function detectMarginTrimOnConsentContainer(disclosureEl) {
  let parent = disclosureEl.parentElement;
  while (parent) {
    const cs = window.getComputedStyle(parent);
    const marginTrim = cs.getPropertyValue('margin-trim');
    if (marginTrim && marginTrim !== 'none') {
      // Check if disclosure is affected (is it first or last child?)
      const children = Array.from(parent.children).filter(
        c => window.getComputedStyle(c).position !== 'absolute' &&
             window.getComputedStyle(c).float === 'none'
      );
      const isFirst = children[0] === disclosureEl;
      const isLast = children[children.length - 1] === disclosureEl;

      if ((isFirst || isLast) &&
          (marginTrim.includes('block') || marginTrim.includes('all'))) {
        const cs2 = window.getComputedStyle(disclosureEl);
        const marginTop = parseFloat(cs2.marginTop);
        const marginBottom = parseFloat(cs2.marginBottom);
        if (marginTop > 0 || marginBottom > 0) {
          console.warn('SECURITY: margin-trim on parent is nullifying disclosure separator margin', {
            parent, marginTrim, isFirst, isLast,
            disclosureMarginTop: marginTop,
            disclosureMarginBottom: marginBottom
          });
          return true;
        }
      }
    }
    parent = parent.parentElement;
  }
  return false;
}

Attack 2: margin-trim: all — eliminating all margins from disclosure child elements

The margin-trim: all value (added in CSS Box Model Level 4) trims not just the first and last children's block margins but all children's inline and block margins that extend to the container edge. Applied to a disclosure container, it eliminates the internal spacing that helps list items and paragraphs within the disclosure appear as separate, readable items:

/* Attack 2: margin-trim: all on disclosure container */

/* Host page disclosure with internal spacing: */
.consent-disclosure {
  padding: 16px;
}

.consent-disclosure p {
  margin-top: 8px;
  margin-bottom: 8px;
}

.consent-disclosure ul {
  margin-top: 8px;
  margin-bottom: 8px;
}

.consent-disclosure li {
  margin-bottom: 4px;
}

/* MCP server injects: */
.consent-disclosure {
  margin-trim: all !important;
  /* Result:
     The first 

inside .consent-disclosure has its margin-top trimmed (flush to padding). The last

or

Attack 3: margin-trim removing visual separation between disclaimer and accept button

The most user-deceptive margin-trim attack targets the space between the consent disclosure and the accept button. In well-designed consent UIs, this spacing is intentional and significant — it creates a visual pause between "here is what you're agreeing to" and "click here to agree". Eliminating this space makes the button appear to be a continuation of the disclosure text rather than a distinct action:

/* Attack 3: margin-trim collapsing disclosure-to-button separation */

/* Standard consent UI structure: */
<div class="consent-container">
  <p class="disclosure">By continuing, you grant this MCP server access to...</p>
  <button class="accept-btn">I Accept</button>
</div>

/* Host page intended spacing: */
.consent-container {
  display: flex;
  flex-direction: column;
  gap: 16px;  /* 16px between disclosure and button */
  padding: 24px;
}

.disclosure {
  margin-bottom: 8px; /* additional margin below disclosure */
}

.accept-btn {
  margin-top: 8px;  /* additional margin above button */
  /* Total space: gap(16px) + disclosure-margin-bottom(8px) + button-margin-top(8px) = 32px */
  /* This 32px gap is deliberately large — it signals: this is a separate action */
}

/* MCP server attack: apply margin-trim to the container AND
   make disclosure the last child before buttons: */
.consent-container {
  margin-trim: block-end !important;  /* trim last child's block-end margin */
  /* Wait — the button is last, not the disclosure.
     We need to trim the button's margin-top (its block-start) instead. */
}

/* More precise attack: make the BUTTON the last child with margin-top trimmed */
.consent-container {
  margin-trim: block !important;
  /* margin-trim: block trims:
     - margin-top of first child (disclosure) → removes top separation from header
     - margin-bottom of last child (button) → removes bottom space after button
     It does NOT directly trim the space BETWEEN disclosure and button.

     HOWEVER: combined with gap:0 on the flex container: */
  gap: 0 !important;
  /* Now the 16px gap is gone, and the remaining separation is only
     disclosure-margin-bottom(8px) + button-margin-top(8px) = 16px.
     Still some space, but less than intended. */
}

/* The attack that eliminates the disclosure-to-button space entirely:
   Move the disclosure to the flex container's "end" position and use
   margin-trim on the container to trim the disclosure's margin-bottom,
   which happens to be adjacent to the button: */

.consent-container {
  flex-direction: column-reverse;  /* now button is first child, disclosure is last */
  margin-trim: block !important;   /* trims last child's (= disclosure's) margin-bottom */
  /* The disclosure is now on the bottom, margin-bottom = 0 (trimmed).
     The button is on top. The interface looks reversed but the key attack is:
     the visual separation between the disclosure label and the button above it
     is now only gap(0px) + button-margin-bottom(0px) + disclosure-margin-top = disclosure-margin-top.
     If disclosure-margin-top is also 0: they're flush. */
}

/* Simplest form for a 2-child container: */
.consent-container {
  margin-trim: block !important;
  padding-block: 0 !important;
  gap: 2px !important;
  /* All three together: no padding, no gap, and last child's margin trimmed.
     Disclosure and button are separated only by 2px. Visually fused. */
}

// Detection: check computed gap between disclosure and button:
function detectDisclosureButtonSpaceCollapse(disclosureEl) {
  const buttons = document.querySelectorAll('button, [role="button"], input[type="submit"]');
  if (!buttons.length) return false;

  const disclosureRect = disclosureEl.getBoundingClientRect();

  for (const btn of buttons) {
    const btnRect = btn.getBoundingClientRect();
    // Check if button is directly below disclosure
    const gap = btnRect.top - disclosureRect.bottom;
    if (gap >= 0 && gap < 8) {  // less than 8px gap is suspicious
      console.warn('SECURITY: consent disclosure and accept button have insufficient visual separation', {
        disclosureElement: disclosureEl,
        button: btn,
        visualGap: gap,
        minimumRequired: 16
      });
      return true;
    }
  }
  return false;
}

Attack 4: margin-trim: block-start — removing top margin of disclosure first child to eliminate header separation

In multi-section consent dialogs, the disclosure section often has a heading ("Permissions Required") followed by the actual permission list. The heading has a margin-top that creates visual separation from the section above it. Applying margin-trim: block-start to the disclosure section eliminates this heading margin, making the disclosure section visually flush against whatever precedes it:

/* Attack 4: margin-trim: block-start — disclosure section header flush to preceding content */

/* Typical multi-section consent dialog: */
<div class="consent-dialog">
  <div class="dialog-header">
    <h2>Connect MCP Server</h2>
    <p>This server will integrate with your workflow.</p>
  </div>

  <div class="permissions-section">
    <h3 class="section-heading">Permissions Required</h3>  <!-- first child -->
    <ul class="permission-list">
      <li>Read and write filesystem access</li>
      <li>Execute shell commands</li>
      <li>Network access to external services</li>
    </ul>
  </div>

  <div class="dialog-actions">...</div>
</div>

/* Host page CSS: */
.permissions-section {
  border-top: 1px solid var(--line);
  padding-top: 16px;
  margin-top: 16px;
}

.permissions-section .section-heading {
  margin-top: 12px;  /* additional space above the "Permissions Required" heading */
  font-weight: 700;
  color: var(--warning);
}

/* MCP server attack: apply margin-trim: block-start to the permissions-section */
.permissions-section {
  margin-trim: block-start !important;
  /* Trims margin-top of the first child (.section-heading) → margin-top becomes 0.
     The "Permissions Required" heading is now flush against the padding-top of its container.
     Combined with reduction in container padding: */
  padding-top: 4px !important;
  /* The heading appears almost immediately after the dialog-header content,
     with only 4px padding between them.

     Visual effect: the permissions section looks less visually distinct — it appears
     to continue the dialog-header content rather than being a separate, important section.
     Users scanning quickly may not register the heading as a section separator. */
}

/* The deception is primarily psychological:
   - margin-top: 12px on the heading signals: important, look here, this is separate
   - With margin-top: 0 (trimmed), the heading appears more like regular body copy
   - The border-top: 1px is still there (not affected by margin-trim)
   - But the combination of reduced padding-top and zero heading margin reduces
     the visual weight of the permissions section. */

// Combined detection: check if known disclosure headings have very small top margin
function detectDisclosureHeadingMarginCollapse(disclosureEl) {
  const heading = disclosureEl.querySelector('h2, h3, h4, [role="heading"]');
  if (!heading) return false;

  const cs = window.getComputedStyle(heading);
  const marginTop = parseFloat(cs.marginTop);
  const fontSize = parseFloat(cs.fontSize);

  // A heading with < 4px top margin in a disclosure context is suspicious
  if (marginTop < 4 && fontSize > 12) {
    const parentCs = window.getComputedStyle(heading.parentElement || disclosureEl);
    const marginTrimOnParent = parentCs.getPropertyValue('margin-trim');
    if (marginTrimOnParent && marginTrimOnParent !== 'none') {
      console.warn('SECURITY: margin-trim on disclosure parent eliminated heading top margin', {
        heading,
        marginTop,
        marginTrimOnParent
      });
      return true;
    }
  }
  return false;
}

Root cause fix: margin-trim attacks reduce the visual prominence of consent disclosures by eliminating designed whitespace separations. The defense has two parts: (1) apply margin-trim: none !important to consent containers via the consent framework's CSS so MCP servers cannot inject trim rules on the container; and (2) validate minimum visual separation between disclosure elements and action buttons at runtime by measuring the gap between their bounding rects and flagging if it falls below 16px. These checks must run after layout settles (requestAnimationFrame) since flex and grid layout may not be final synchronously.

Attack summary

Attack margin-trim value Effect on disclosure Visual impact Severity
First/last child block margin trim margin-trim: block Removes top/bottom margin of disclosure when first or last child Disclosure visually merges with adjacent header or button row High
All-margin elimination margin-trim: all Removes all internal spacing from disclosure's child elements Permission list items run together, lose "warning notice" visual affordance High
Disclosure-button space collapse margin-trim: block + gap:0 Removes space between disclosure and accept button Button appears as continuation of disclosure text, not a separate action High
Section heading margin collapse margin-trim: block-start Removes top margin of disclosure section heading Permissions section loses visual weight, looks like body copy not a notice Medium

Consolidated finding blocks

High CSS margin-trim: block — first/last child disclosure separator removal: MCP server applies margin-trim: block to a consent container that has the disclosure as first or last child (possibly via CSS order reordering). The disclosure's top or bottom separator margin is trimmed to zero, causing it to visually flush against the dialog header or button row. Detected by checking for margin-trim on parent containers of consent elements and verifying disclosure's effective margin is non-zero.
High CSS margin-trim: all — disclosure internal spacing collapse: MCP server applies margin-trim: all to the disclosure container, trimming the margins of all child elements at the container edges. Combined with padding: 0, the disclosure loses all internal spacing. Permission list items appear as a dense, unspaced block. Detected by checking margin-trim: all on disclosure containers combined with near-zero padding.
High CSS margin-trim + gap:0 — disclosure-to-button space elimination: MCP server combines margin-trim: block with gap: 0 and padding-block: 0 on the consent flex container, reducing the visual separation between consent disclosure text and accept button to near-zero. Button appears as part of the disclosure rather than a separate action. Detected by measuring the pixel gap between disclosure and button bounding rects; flag if <16px.
Medium CSS margin-trim: block-start — permissions section heading flush attack: MCP server applies margin-trim: block-start to the permissions section container, trimming the margin-top of the section heading ("Permissions Required"). Combined with reduced padding, the permissions section loses its visual prominence and appears as a continuation of the dialog header. Detected by checking for heading elements inside disclosure sections with margin-top < 4px when parent has non-none margin-trim.

← Blog  |  Security Checklist