MCP server CSS break-before security: break-before:page print exile, break-before:column off-screen column push, @page named-page zero-size box, and break-after:right blank page insertion attacks
Published 2026-07-25 — SkillAudit Research
CSS fragmentation break properties — break-before, break-after, and break-inside — control where a content box fragments across pages, columns, or regions. The critical security characteristic is that forced-break values (page, column, left, right, recto, verso, region) have no visible effect on screen rendering. They only take effect in paged-media contexts (print dialog, PDF export, headless-Chrome rendering) and in multi-column layout. An element with break-before: page looks and passes all screen-based security checks normally — the attack manifests exclusively in print/PDF output.
This screen/print dichotomy makes fragmentation attacks particularly dangerous for consent management systems that generate PDF audit trails as compliance records. An MCP server can apply break-before: page to the consent disclosure container; the consent renders normally on screen for the user interaction, but the PDF audit record generated via window.print() or headless Chrome has the consent text on a separate page that can be suppressed, blank-padded, or routed to a zero-size page box. The DOM contains the consent text; the legally significant PDF record does not.
Detection gap: break-before: page is undetectable via getComputedStyle(el).display, .opacity, .visibility, .transform, or getBoundingClientRect(). Standard MCP security scanners do not check fragmentation properties. Detection requires explicitly checking cs.breakBefore, scanning CSSPageRule instances for named-page zero-size declarations, and running a headless-Chrome print simulation with page-presence verification.
Attack 1: break-before:page forces consent to a separate print page — combined with @media print display:none
break-before: page applied to the consent element forces a page break immediately before the consent in any paged rendering context. The consent element starts at the top of page 2 (or later). The MCP server then applies @media print { .consent-disclosure { display: none } } or visibility: hidden to remove the consent from the print output on that separate page. The two rules work together: the consent is moved to its own page (via break-before: page), then that page's content is cleared (via @media print rules). On screen, break-before: page has no effect and the @media print display:none is not applied — the consent is fully visible. In the PDF output, the consent page is blank.
/* MCP attack: */
.consent-disclosure {
break-before: page; /* screen: no effect — print: consent on page 2 */
}
@media print {
.consent-disclosure {
display: none; /* removes consent from its isolated page */
}
/* Combined effect: the print output has:
Page 1 — product content (fully present)
Page 2 — blank (consent was here but display:none removed it)
The PDF audit record contains no consent text. */
}
// Detection: check computed break-before and @media print rules
function detectPageBreakExile() {
const consent = document.querySelector('.consent-disclosure, #consent-panel, [data-consent]');
if (!consent) return;
const cs = window.getComputedStyle(consent);
if (!['auto', 'avoid', 'avoid-page', ''].includes(cs.breakBefore)) {
console.error('SECURITY: break-before on consent element:', cs.breakBefore);
}
// Scan @media print rules
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSMediaRule && rule.conditionText === 'print') {
for (const inner of rule.cssRules) {
if (/consent|disclosure/i.test(inner.selectorText || '')) {
if (inner.style.display === 'none') {
console.error('SECURITY: @media print hides consent:', inner.selectorText);
}
}
}
}
}
} catch (e) {}
}
}
Attack 2: break-before:column forces consent to off-screen column overflow
In a multi-column layout, break-before: column forces the element to start at the beginning of the next column. The MCP server combines this with an extreme column-gap value to place the next column beyond the viewport, or uses a single-column container (column-count: 1) with overflow: hidden so that the forced column break places consent in the overflowed region. The consent heading — styled to stay in the first column without a forced break — remains visible, while the consent body with break-before: column is forced into an invisible overflow column.
/* MCP attack — break-before:column with off-screen column gap: */
.consent-container {
column-count: 2;
column-gap: 9999px; /* second column starts 9999px off-screen */
}
.consent-heading {
/* no break-before — stays in column 1 (visible) */
}
.consent-body {
break-before: column; /* forced into column 2 — 9999px off-screen */
/* Consent heading is visible: "Privacy Notice"
Consent body is invisible: the actual data-sharing and legal terms */
}
// Detection
function detectColumnBreakPush() {
const bodies = document.querySelectorAll(
'.consent-body, [data-consent-body], .disclosure-text'
);
bodies.forEach(el => {
const cs = window.getComputedStyle(el);
if (cs.breakBefore === 'column' || cs.breakBefore === 'always') {
const parent = el.parentElement;
const pcs = window.getComputedStyle(parent);
if (parseFloat(pcs.columnGap) > 100) {
console.error('SECURITY: break-before:column pushes consent to off-screen column', {
el, columnGap: pcs.columnGap
});
}
}
});
}
Attack 3: @page named-page with size:0mm routes consent to zero-size page box
CSS named pages allow an element to be rendered on a page with a specific @page rule. The page CSS property on an element assigns it to a named page: page: consent-hidden. If @page consent-hidden { size: 0mm 0mm; margin: 0; } is defined, the consent is routed to a page box with zero dimensions — the browser creates a page but renders nothing within it. The consent element is fully present in the DOM; it is never rendered in the print output because the page box assigned to it has no renderable area. On screen, the page property has no effect — the element renders normally.
/* MCP attack — zero-size named page: */
@page consent-hidden {
size: 0mm 0mm;
margin: 0;
/* Zero-size page box — no content renders on this page.
Browser creates the page but nothing within it is painted. */
}
.consent-disclosure {
page: consent-hidden; /* screen: no effect — print: routed to 0x0 page */
}
/* Even without explicit display:none, no consent text appears in PDF output.
The page exists (page count increases by 1) but contains nothing.
A reviewer sees an extra blank page with no content. */
// Detection
function detectZeroSizeNamedPage() {
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSPageRule) {
const size = rule.style.getPropertyValue('size');
if (size && /\b0(mm|px|cm|pt|in)\b/.test(size)) {
console.error('SECURITY: @page zero-size page box:', rule.selectorText, size);
}
}
}
} catch (e) {}
}
document.querySelectorAll('.consent-disclosure, #consent-panel, [data-consent]').forEach(el => {
const page = window.getComputedStyle(el).getPropertyValue('page');
if (page && page !== 'auto') {
console.warn('SECURITY: consent element assigned to named page:', page);
}
});
}
Attack 4: break-after:right / break-before:left blank page insertion chain
CSS page break directions — break-before: right (element must start on right-hand page), break-before: left (element must start on left-hand page) — force the browser to insert blank pages when alignment requires it. An MCP server creates a chain: the section before consent uses break-after: right (next content must be on a right page), and the consent itself uses break-before: left (consent must be on a left page). If the current page is left, this inserts: blank-right → consent-left. If current is right: blank-left → blank-right → consent-left. Each blank page in the chain has only running headers/footers, no main content. Users paging through a print preview after the first blank page assume the document has ended and stop reviewing. The consent text exists on the last page but is never reached in practice.
/* MCP attack — blank page insertion chain: */
.product-summary {
break-after: right; /* next section must start on a right page */
}
/* Gap section between product and consent: */
.terms-intro {
break-before: right; /* this section is on a right page */
break-after: page; /* forces page break after it */
}
.consent-disclosure {
break-before: left; /* consent must be on a left page */
/* Chain result (starting from page 1 = right):
P1(right): product summary
P2(left): [blank — inserted to satisfy break-after:right for next]
P3(right): terms intro
P4(left): consent disclosure
Users see pages 1, 2(blank), 3, 4 in print preview.
After blank page 2 most users close the print dialog. */
}
// Detection
function detectBlankPageChain() {
const els = document.querySelectorAll('*');
const breakValues = ['left', 'right', 'recto', 'verso'];
els.forEach(el => {
const cs = window.getComputedStyle(el);
const bb = cs.breakBefore;
const ba = cs.breakAfter;
if (breakValues.includes(bb) || breakValues.includes(ba)) {
const isConsentAdjacent = el.closest('.consent-disclosure, #consent-panel, [data-consent]') ||
el.nextElementSibling?.matches('.consent-disclosure, #consent-panel, [data-consent]') ||
el.previousElementSibling?.matches('.consent-disclosure, #consent-panel, [data-consent]');
if (isConsentAdjacent) {
console.warn('SECURITY: left/right page break near consent element — may insert blank pages:', {
el, breakBefore: bb, breakAfter: ba
});
}
}
});
}
All four attacks are invisible on screen: break-before: page, break-before: column, named-page assignment, and break-after:right chains all have zero visual effect on screen rendering. The attacks only manifest in print/PDF output. Standard consent audits that check screen rendering will miss all four variants. SkillAudit's audit engine checks cs.breakBefore, cs.breakAfter, cs.page, and @page rule sizes, and additionally runs a headless-Chrome print capture to verify consent text appears on page 1 of the PDF output.
Attack summary
| Attack | Properties | What is suppressed | Severity |
|---|---|---|---|
| break-before:page + @media print display:none | break-before:page + print media rule | All consent in PDF output | High |
| break-before:column + column-gap:9999px | break-before:column + column-gap | Consent body (heading visible) | High |
| Named @page with size:0mm | page: name + @page { size: 0mm } | All consent on zero-size page | High |
| break-after:right blank page chain | break-after:right + break-before:left | Consent buried after blank pages | Medium |
Consolidated finding blocks
break-before: page to the consent container, moving it to a separate page in paged media. A companion @media print { display: none } rule removes the consent from that page. On screen, consent renders normally — the attack is invisible to user-visible checks. The PDF audit log generated by compliance systems has the consent page blank. GDPR Article 5(2) accountability documentation is corrupted.
break-before: column to the consent body element within a multi-column container with column-gap: 9999px. The consent heading ("Privacy Notice") appears in column 1 (visible). The consent body (data-sharing terms, legal basis, user rights) is forced into column 2 starting 9999px off-screen. No overflow: hidden is needed — the gap itself places column 2 beyond the viewport. Users see only the heading, not the consent content.
page: consent-hidden and defines @page consent-hidden { size: 0mm 0mm; margin: 0 }. The consent is routed to a page box with zero dimensions; no content renders on this page in the PDF output. The DOM is intact; the PDF contains no consent text. Detection requires scanning all CSSPageRule instances for zero-size size declarations.
Deep-dive blog post | CSS column-fill security | Security Checklist