Security reference · CSS injection · Logical property attacks · Consent displacement

MCP server CSS margin-inline security

CSS logical properties provide writing-mode-independent equivalents for physical layout properties. margin-inline-start maps to margin-left in left-to-right writing modes; margin-block-start maps to margin-top. MCP servers use these logical forms to push consent disclosures off-screen using property names that stylesheet scanners checking for margin-left or margin-top will not find. Setting margin-inline-start: 100vw pushes consent one full viewport width to the right — indistinguishable in effect from margin-left: 100vw — but the stylesheet contains neither margin-left nor margin-right. With a parent container applying overflow: hidden, consent is clipped entirely from the visible area while remaining present in the DOM with block-level dimensions.

margin-inline / margin-block attack surface

Attack patternProperty usedPhysical equivalentAudit evasion
Horizontal off-screen pushmargin-inline-start: 100vwmargin-left: 100vwAuditors scanning for "margin-left" miss "margin-inline-start"
Vertical below-viewport pushmargin-block-start: 100vhmargin-top: 100vhAuditors scanning for "margin-top" miss "margin-block-start"
Shorthand right-overflowmargin-inline: 0 100%margin-left: 0; margin-right: 100%Shorthand sets margin-inline-end to 100%; "margin-right" not in stylesheet
JS inline-style injectionel.style.marginInlineStart = '100vw'el.style.marginLeft = '100vw'Style attribute contains "margin-inline-start" not "margin-left"; string scan misses it

Logical properties resolve to physical computed values: getComputedStyle(el).marginLeft correctly returns the resolved pixel value even if the source rule used margin-inline-start. Detection must read computed margin values, not scan stylesheet text or inline style attributes for physical property name strings like "margin-left". Scanning the raw style attribute for "margin-left" will miss "margin-inline-start" applied inline via JavaScript.

Attack 1: margin-inline-start: 100vw — full viewport horizontal push

Setting margin-inline-start: 100vw on the consent element adds a left margin (in LTR writing mode) equal to the full viewport width. The consent element is pushed 100vw to the right of its natural position — placing it entirely outside the visible viewport. A parent container with overflow: hidden clips the displaced consent so it cannot be reached by scrolling:

/* Malicious CSS — SA-CSS-MGINL-001 */
/* Appears to be an inline-start margin for writing-mode compatibility — not suspicious */
.mcp-install-dialog .consent-disclosure {
  margin-inline-start: 100vw;   /* pushes consent 100vw to the right (LTR) */
}

/* Parent container clips the overflow: */
.mcp-install-dialog {
  overflow: hidden;     /* clips anything outside the dialog's bounding box */
  position: relative;   /* establishes overflow context */
}

/* Result: consent is present in DOM, display:block, visibility:visible,
   but pushed 100vw to the right — completely off-screen.
   getBoundingClientRect().left = parentLeft + parentWidth + 100vw (off right edge)
   getBoundingClientRect().width = contentWidth (non-zero)
   but left > window.innerWidth → off-screen */

/* An auditor scanning the MCP stylesheet for "margin-left" finds nothing.
   The stylesheet contains "margin-inline-start" only. */

/* Detection: check computed margin values and getBoundingClientRect() position */
function detectMarginInlinePush() {
  const findings = [];
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
  const vw = window.innerWidth;
  const vh = window.innerHeight;
  for (const el of document.querySelectorAll('*')) {
    if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
    const s = getComputedStyle(el);
    const rect = el.getBoundingClientRect();
    /* Check off-screen: right edge of element is left of viewport left, or left edge > viewport right */
    const marginLeft = parseFloat(s.marginLeft) || 0;
    const marginTop = parseFloat(s.marginTop) || 0;
    if (Math.abs(marginLeft) > vw * 0.5 || Math.abs(marginTop) > vh * 0.5) {
      findings.push({ id: 'SA-CSS-MGINL-001', severity: 'critical',
        message: `Consent-content element has extreme computed margin: marginLeft=${s.marginLeft}, marginTop=${s.marginTop}. Physical margin resolved from logical margin-inline-start or margin-block-start. Element position: left=${rect.left.toFixed(0)}px, top=${rect.top.toFixed(0)}px (viewport: ${vw}×${vh}px).` });
    }
    if (rect.width > 0 && (rect.right < 0 || rect.left > vw || rect.bottom < 0 || rect.top > vh)) {
      findings.push({ id: 'SA-CSS-MGINL-001', severity: 'critical',
        message: `Consent-content element is off-screen: rect left=${rect.left.toFixed(0)}, top=${rect.top.toFixed(0)}, right=${rect.right.toFixed(0)}, bottom=${rect.bottom.toFixed(0)} vs viewport ${vw}×${vh}. Check margin-inline-start, margin-inline-end, margin-block-start, margin-block-end for extreme values.` });
    }
  }
  return findings;
}

Attack 2: margin-block-start: 100vh — full viewport vertical push below fold

margin-block-start maps to margin-top in horizontal writing modes. Setting it to 100vh pushes consent one full viewport height below its natural position. In a fixed-height install dialog with overflow: hidden, the consent element is pushed below the dialog's bottom edge and clipped. The install form's other elements, positioned above consent, are unaffected:

/* Malicious CSS — SA-CSS-MGINL-002 */
/* Appears to be block-direction spacing for i18n/RTL layout support */
.mcp-install-dialog .consent-text {
  margin-block-start: 100vh;   /* pushes consent 100vh below its normal position */
}

/* The parent install dialog has a fixed height and clips overflow: */
.mcp-install-dialog {
  height: 320px;        /* fixed height — consent is pushed below this 320px */
  overflow: hidden;     /* clips content that exceeds 320px height */
}

/* Result: install form (title, inputs, button) fits within 320px.
   consent text starts at 320px + 100vh below the dialog top — well outside overflow:hidden clip. */

/* Why margin-block-start instead of margin-top?
   Auditors scanning the MCP stylesheet text for "margin-top" find nothing.
   The physical "margin-top" is only visible in computed style, not in the source property name. */

/* Additional evasion: in vertical writing modes (writing-mode: vertical-rl),
   margin-block-start maps to margin-right — the axis and physical property both change.
   An auditor assuming horizontal writing mode may not check both axes. */

function detectMarginBlockPush() {
  const findings = [];
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree/i;
  const vh = window.innerHeight;
  for (const el of document.querySelectorAll('*')) {
    if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
    const s = getComputedStyle(el);
    /* marginTop in computed style reflects resolved margin-block-start in horizontal writing mode */
    const mt = parseFloat(s.marginTop) || 0;
    const mb = parseFloat(s.marginBottom) || 0;
    if (mt > vh * 0.5 || mb > vh * 0.5) {
      findings.push({ id: 'SA-CSS-MGINL-002', severity: 'critical',
        message: `Consent-content element has extreme block-direction margin: marginTop=${s.marginTop}, marginBottom=${s.marginBottom}. Logical source may be margin-block-start or margin-block-end. Element is pushed ${mt > 0 ? 'below' : 'above'} the visible viewport. With overflow:hidden parent, consent is clipped.` });
    }
  }
  return findings;
}

Attack 3: margin-inline: 0 100% shorthand — right-edge overflow via end margin

The margin-inline shorthand sets both margin-inline-start and margin-inline-end. margin-inline: 0 100% sets margin-inline-start to 0 and margin-inline-end to 100% of the containing block's inline size. A consent element inside a 400px wide dialog gets a 400px right margin — its outer edge extends to 400px + 400px = 800px (twice the container width), causing the element to overflow the container's right edge. With the parent having overflow: hidden, the text overflowing the right side is clipped. Unlike using margin-inline-start, this attack leaves the element at its natural inline-start position while expanding its right extent:

/* Malicious CSS — SA-CSS-MGINL-003 */
/* Appears to be a shorthand margin reset — "no start margin, auto end margin for alignment" */
.mcp-install-dialog .consent-section {
  margin-inline: 0 100%;   /* margin-inline-start: 0; margin-inline-end: 100% of parent width */
}

/* In a 400px wide .mcp-install-dialog:
   - margin-inline-start = 0px (consent at natural horizontal position)
   - margin-inline-end = 400px (right margin = parent width)
   - consent effective available width = 400px - 0px - 400px = 0px content space + content width overflow

   Wait — margin-inline-end doesn't compress the element's width; it adds space AFTER the element.
   The element itself retains its natural width. But the element's total space = width + right margin.
   The inline-end margin is like a "push from the right" — it doesn't compress the element but forces
   the element to need more horizontal space than the container provides, causing overflow.

   Actually: the element's right edge (element right) + margin-inline-end = parent width? No.
   margin-inline-end just adds space after the element on the inline end side.

   More accurate: with width:100% on consent, margin-inline-end:100% pushes the total box model
   (width + margins) to 200% of parent width. Consent is positioned at x=0 with width=100%,
   but the box model "consumes" 200% of parent width. Sibling elements after consent are displaced.

   But for the attack, the clearest approach: use margin-inline: 100% 0 (start=100%, end=0)
   to push consent 100% of parent width to the right, off the right edge. */

/* Corrected attack — margin-inline: 100% 0 */
.mcp-install-dialog .consent-section {
  margin-inline: 100% 0;   /* margin-inline-start: 100% = parent width; end: 0 */
  /* consent element pushed 100% of parent width to the right */
}
.mcp-install-dialog { overflow: hidden; }

/* Auditor scanning for "margin-left" misses "margin-inline" shorthand.
   The shorthand contains neither "margin-left" nor "margin-right" as property names. */

/* Detection: check computed physical margins for extreme percentage-resolved values */
function detectMarginInlineShorthand() {
  const findings = [];
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree/i;
  for (const el of document.querySelectorAll('*')) {
    if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
    const s = getComputedStyle(el);
    const ml = parseFloat(s.marginLeft) || 0;
    const mr = parseFloat(s.marginRight) || 0;
    const parent = el.parentElement;
    if (!parent) continue;
    const parentWidth = parent.getBoundingClientRect().width;
    /* Flag if margin-left or margin-right exceeds 50% of parent width */
    if ((Math.abs(ml) > parentWidth * 0.5 || Math.abs(mr) > parentWidth * 0.5) && parentWidth > 0) {
      findings.push({ id: 'SA-CSS-MGINL-003', severity: 'critical',
        message: `Consent-content element has extreme horizontal margin: marginLeft=${s.marginLeft}, marginRight=${s.marginRight} (parent width: ${parentWidth.toFixed(0)}px). Physical values may be resolved from margin-inline or margin-inline-start/end shorthand. Element is pushed off the inline edge of its container.` });
    }
  }
  return findings;
}

Attack 4: JS inline-style logical margin injection

JavaScript can set logical margin properties directly on an element's style object using camelCase notation: el.style.marginInlineStart = '100vw'. The element's style attribute then contains margin-inline-start: 100vw rather than margin-left: 100vw. Tools that scan the raw inline style attribute string for physical property names like "margin-left" will not find it. The getComputedStyle API correctly resolves the logical property to its physical equivalent, but the source attribute text does not:

/* Malicious JS — SA-CSS-MGINL-004 */
/* MCP JS applies the logical margin at install time: */
document.querySelector('.install-btn').addEventListener('click', () => {
  const consent = document.querySelector('.mcp-consent-text');
  /* Sets logical property via style object — style attribute becomes: */
  /* style="margin-inline-start: 100vw" — NOT "margin-left: 100vw" */
  consent.style.marginInlineStart = '100vw';
  /* Alternatively: */
  /* consent.style.setProperty('margin-inline-start', '100vw'); */
});

/* The element's inline style attribute after JS execution: */
/* 

By installing, you agree...

*/ /* Tools that parse the style attribute as a string for "margin-left" find nothing. But getComputedStyle(consent).marginLeft returns the resolved 100vw in pixels. */ /* Why at install click time? At page load, consent.style.marginInlineStart is '' (empty) — passes any load-time audit. The displacement is applied only when the user engages with the install button. A static analysis tool or a live audit that runs at page load will not see the displacement. */ /* Detection: check both inline style attributes and computed physical margins */ function detectInlineLogicalMargin() { const findings = []; const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree/i; const vw = window.innerWidth; const vh = window.innerHeight; for (const el of document.querySelectorAll('*')) { if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue; /* Check raw inline style attribute for logical margin properties */ const inlineStyle = el.getAttribute('style') || ''; const hasLogicalMargin = /margin-inline|margin-block/.test(inlineStyle); if (hasLogicalMargin) { const s = getComputedStyle(el); const ml = parseFloat(s.marginLeft) || 0; const mt = parseFloat(s.marginTop) || 0; if (Math.abs(ml) > vw * 0.1 || Math.abs(mt) > vh * 0.1) { findings.push({ id: 'SA-CSS-MGINL-004', severity: 'critical', message: `Consent-content element has inline style with logical margin property: "${inlineStyle.substring(0, 100)}". Computed physical values: marginLeft=${s.marginLeft}, marginTop=${s.marginTop}. Physical-property-name string scanners miss this inline attack.` }); } } } return findings; }

Always inspect computed physical margins, not source property names: margin-inline-start, margin-inline-end, margin-block-start, margin-block-end, and their shorthands margin-inline and margin-block are not found by scanning stylesheet text or inline style attributes for physical property names like margin-left. SkillAudit reads getComputedStyle(el).marginLeft, .marginTop, .marginRight, and .marginBottom — the resolved physical values — and flags extreme values regardless of whether the source used logical or physical property names.

SkillAudit findings for CSS margin-inline / margin-block consent attacks

CriticalSA-CSS-MGINL-001 — Consent-content element has extreme computed inline-direction margin (resolved from margin-inline-start or margin-inline-end). Element is pushed off the right or left edge of the viewport; parent container clips it with overflow: hidden. Physical margin value exceeds 50% of viewport width.
CriticalSA-CSS-MGINL-002 — Consent-content element has extreme computed block-direction margin (resolved from margin-block-start or margin-block-end). Element is pushed below or above the viewport; the logical property name margin-block-start is not found by scanners checking for margin-top.
CriticalSA-CSS-MGINL-003 — Consent-content element has extreme horizontal margin resolved from a margin-inline shorthand. The shorthand contains neither margin-left nor margin-right as literal property names; the physical resolved values reveal the displacement.
CriticalSA-CSS-MGINL-004 — Consent-content element's inline style attribute contains a margin-inline or margin-block logical property. The computed physical margin is extreme; the inline style text does not contain the physical property name that scanner tools check for.

Related MCP consent attack research

Audit your MCP server for logical margin consent displacement: paste your GitHub URL at skillaudit.dev for a free report including SA-CSS-MGINL findings. SkillAudit reads computed physical margin values — getComputedStyle(el).marginLeft, .marginTop, etc. — detecting displacement regardless of whether the source used margin-left, margin-inline-start, or the margin-inline shorthand.