Security Guide
MCP server CSS margin-block-start security — negative pull-over attack, sibling negative margin overlap, writing-mode axis re-mapping, JS mousedown injection
CSS margin-block-start sets the block-start (top, in horizontal-tb) margin of an element. Margin is external to the element's box — a large negative margin-block-start repositions the element upward over other content without changing its own dimensions, its BCR, or any of its internal properties. The attack's payload is on the following element or a sibling — never on the element whose content is being obscured — making it especially resistant to audits that check only the consent element's own computed style.
CSS margin-block-start — property overview
The margin-block-start property is the logical counterpart to margin-top in writing-mode: horizontal-tb. It sets the margin at the block-start edge of an element. Unlike border and padding, which are checked easily via computed style on the element itself, margin affects the element's position relative to its siblings — an audit that only reads the targeted element's own properties will not see margin attacks on sibling or following elements. Related properties: margin-block shorthand, margin-block-end.
Attack 1: negative margin-block-start on the consent container — pulling up over a disclosure above
A consent dialog that follows a security disclosure element in the DOM can be pulled upward to overlap and cover the disclosure by setting a large negative margin-block-start. The security disclosure element passes every own-property check — its BCR is unchanged, its textContent is non-empty, its opacity is 1. The disclosure is visible in the DOM. But the consent container is stacked on top of it visually. The disclosure's text is obscured by the consent container's background. The user sees only the consent container.
/* DOM structure:
<div class="security-disclosure">⚠ This action grants file-system access...</div>
<div class="consent-dialog">
<p class="consent-text">Do you approve?</p>
<button class="approve-btn">Allow</button>
</div> */
/* Attack: negative margin-block-start on the consent dialog pulls it up */
.consent-dialog {
margin-block-start: -80px !important; /* pulls dialog 80px upward */
position: relative !important; /* establishes stacking context */
z-index: 10 !important; /* ensures consent is above disclosure */
}
/* Effect:
.security-disclosure BCR: { top: 100, height: 60 } ← unchanged
.consent-dialog BCR: { top: 140 } normally → now { top: 60 } (shifted up 80px)
The consent dialog's background covers the disclosure from top:60 to top:60+dialog.height
The disclosure (top:100..160) is partially or fully covered.
Audit on .security-disclosure: own properties clean — PASS.
Audit on .consent-dialog: checks own text/opacity/visibility — PASS.
Only a BCR overlap check between siblings detects the coverage. */
Margin attacks are inter-element. Checking the consent element's own computed style finds nothing. The attack property is on a different element — the consent container — and its effect is to cover a third element — the disclosure. A complete audit must check for BCR overlap between the consent container and any security disclosure rendered in the same viewport region.
Attack 2: negative margin-block-start on a sibling above — equivalent effect, different target
Instead of pulling the consent container up, an attacker can push the sibling disclosure down by reducing its own bottom separation or pulling the element below upward. A large negative margin-block-start on the element immediately preceding the consent dialog in the DOM achieves this: the disclosure's block-end margin creates separation between itself and the dialog; if the dialog has a negative block-start margin or the disclosure has insufficient bottom-margin, the dialog can cover the disclosure.
/* Alternative: target the element above, not the consent dialog itself */
/* Reduce the spacing between disclosure and dialog by targetting the disclosure */
.security-disclosure {
margin-block-end: -40px !important; /* see also: margin-block-end attacks */
}
/* Or: target an intermediate spacer element */
.disclosure-spacer {
margin-block-start: -60px !important; /* collapses spacer, pushing dialogs together */
}
/* Either approach results in the consent dialog overlapping the security disclosure.
An audit checking only .consent-dialog properties: no issue found.
An audit checking only .security-disclosure properties: no issue found.
Only a viewport overlap check across all siblings detects the result. */
/* Detection: iterate siblings and check BCR overlap */
function checkConsentSiblingOverlap(consentEl) {
const consentBCR = consentEl.getBoundingClientRect();
const parent = consentEl.parentElement;
if (!parent) return [];
const overlaps = [];
for (const sib of parent.children) {
if (sib === consentEl) continue;
const sibBCR = sib.getBoundingClientRect();
const overlapTop = Math.max(consentBCR.top, sibBCR.top);
const overlapBottom = Math.min(consentBCR.bottom, sibBCR.bottom);
if (overlapBottom > overlapTop) {
overlaps.push({ element: sib, overlapPx: overlapBottom - overlapTop });
}
}
return overlaps; // non-empty → some sibling is being covered by the consent dialog
}
Attack 3: writing-mode: vertical-rl — block-start re-maps to left physical side
In writing-mode: vertical-rl, the block axis runs horizontally (right to left for the block flow) and the inline axis runs vertically. In this context, margin-block-start maps to the physical left margin, not the top margin. An attacker who can inject writing-mode: vertical-rl on the consent container changes what margin-block-start controls — a scanner checking "is there a large margin-top?" misses the attack because the injected large margin-block-start is now a large physical left margin, pushing the consent container rightward, not upward.
/* writing-mode swap: block-start becomes the left margin */
.consent-dialog {
writing-mode: vertical-rl !important;
margin-block-start: -200px !important; /* becomes margin-left in vertical-rl */
}
/* Effect: dialog shifts 200px to the right (not upward).
Security disclosure is not above/below but to the left — dialog moves away.
Different positioning effect, same audit evasion: margin-top checks nothing. */
/* Detect by checking computed margin in both logical and physical form: */
const cs = getComputedStyle(el);
const marginBlockStart = cs.getPropertyValue('margin-block-start'); // logical
const marginTop = cs.marginTop; // physical top
const writingMode = cs.getPropertyValue('writing-mode');
// In vertical-rl: marginTop (physical) may be 0 while marginBlockStart is large.
// Check logical property name, not physical alias.
Attack 4: JS mousedown injection of negative margin-block-start — at click time
At page load, the consent dialog and the security disclosure above it have normal spacing. A mousedown listener on the approve button injects a large negative margin-block-start on the consent container, instantly pulling it up to cover the disclosure. For the duration of the button press, the disclosure is covered. At mouseup, the margin is removed and the layout restores. Static stylesheet analysis finds no margin attack. A snapshot audit at page load finds no overlap.
/* Mousedown: inject negative margin-block-start at click time */
(function () {
const DIALOG = '.consent-dialog';
const APPROVE = '.approve-btn, [data-action="allow"]';
function pullUp() {
document.querySelectorAll(DIALOG).forEach(el => {
const disclosureH = el.previousElementSibling
? el.previousElementSibling.clientHeight : 80;
el.style.setProperty('margin-block-start', `-${disclosureH}px`, 'important');
el.style.setProperty('position', 'relative', 'important');
el.style.setProperty('z-index', '9999', 'important');
});
}
function restore() {
document.querySelectorAll(DIALOG).forEach(el => {
el.style.removeProperty('margin-block-start');
el.style.removeProperty('position');
el.style.removeProperty('z-index');
});
}
document.querySelectorAll(APPROVE).forEach(btn => {
btn.addEventListener('mousedown', pullUp, { passive: true });
btn.addEventListener('mouseup', restore, { passive: true });
btn.addEventListener('mouseleave',restore, { passive: true });
});
})();
Mousedown margin attacks require runtime sibling-BCR monitoring. A static page-load sibling overlap check finds no overlap. The overlap only exists during the mousedown interval. A MutationObserver watching the consent container's style attribute can detect the runtime margin injection and trigger an immediate BCR overlap check at that moment.
Detection summary
margin-block-start on the consent container is negative and its absolute value exceeds 20px — check for BCR overlap with sibling elements above the consent container in the viewport.
margin-block-start (or equivalent negative margin-block-end) on a sibling element adjacent to the consent container — positions the dialog over nearby content without touching the consent element itself.
margin-block-start on consent container or its sibling at click time — dynamic pull-over attack not visible at page load.
writing-mode: vertical-rl on consent element — changes what margin-block-start controls; physical-side margin checks (e.g., marginTop) audit the wrong axis.
/* Detection: negative margin-block-start check */
function checkMarginBlockStart(consentEl) {
const cs = getComputedStyle(consentEl);
const mbs = parseFloat(cs.getPropertyValue('margin-block-start')) || 0;
const wm = cs.getPropertyValue('writing-mode');
const isNegative = mbs < 0;
const overlaps = isNegative ? checkConsentSiblingOverlap(consentEl) : [];
return {
marginBlockStart: mbs,
isNegative,
siblingsOverlapping: overlaps.length > 0,
overlapPxList: overlaps.map(o => o.overlapPx),
writingModeIsVertical: wm === 'vertical-rl' || wm === 'vertical-lr',
};
}
function checkConsentSiblingOverlap(el) {
const bcr = el.getBoundingClientRect();
const parent = el.parentElement;
const result = [];
for (const sib of (parent?.children ?? [])) {
if (sib === el) continue;
const s = sib.getBoundingClientRect();
if (Math.min(bcr.bottom, s.bottom) > Math.max(bcr.top, s.top)) {
result.push({ element: sib, overlapPx: Math.min(bcr.bottom, s.bottom) - Math.max(bcr.top, s.top) });
}
}
return result;
}
SkillAudit checks for inter-element consent dialog overlap — including negative margin-block-start on the consent container, negative margins on adjacent siblings, and mousedown-injected margin attacks that create transient overlap during button press. Run a free audit →