MCP server CSS break-after security: break-after:page on install button separates consent from action in PDF, break-after:column in multi-column, @media print injection, and JS beforeprint attacks
Published 2026-08-07 — SkillAudit Research
CSS break-after specifies a break condition immediately after the element to which it is applied. This is structurally different from break-before attacks (where the break property is on the consent element itself) — break-after attacks place the break property on the element preceding the consent disclosure. When the install button has break-after: page, the PDF audit trail places the install button on page 1 and the consent disclosure on page 2. Scanners that check only the consent element's computed style find no suspicious properties. This predecessor-targeting pattern makes break-after attacks significantly harder to detect than break-before.
The attack is specific to paged and columnar layout contexts (print dialog, PDF capture, headless Chrome printing, multi-column layouts). On screen, break-after: page has no visual effect — the install dialog renders identically with or without the property. Compliance audit systems that capture PDF records of the install consent flow are the primary target: the PDF shows the install button on page 1 with no adjacent consent; the consent appears alone on page 2 after the action has been recorded.
Detection gap: Checking getComputedStyle(consentElement).breakAfter returns auto — the attack is not on the consent element. Detection requires checking getComputedStyle(precedingElement).breakAfter and getComputedStyle(precedingElement).breakBefore for all elements immediately before and containing the consent disclosure in document order.
Attack 1: break-after:page on install button — consent on page 2 of PDF audit trail (SA-CSS-BKAF-001)
The install button element receives break-after: page. In the screen layout, the install button and consent disclosure are visually adjacent — the user sees both. In the print/PDF output, the page break fires immediately after the install button: page 1 contains all product content and the install button; page 2 contains the consent disclosure. Compliance audit systems that generate PDF records during or after the install flow capture this layout. Reviewers checking the PDF audit record see the install action on page 1 with no associated consent, and the consent on a subsequent page disconnected from the action it authorizes.
/* MCP attack: */
#install-btn {
break-after: page;
/* Screen: no visual effect — button and consent both visible
PDF/print: page break after button
Page 1: product info + install button (action)
Page 2: consent disclosure (authorization)
— action and authorization are on separate pages in audit record */
}
.consent-disclosure {
/* no break properties — passes consent-element scan */
}
// Detection: check preceding siblings and nearby elements, not just consent element
function detectBreakAfterOnPrecedingElements() {
const consentEls = document.querySelectorAll(
'.consent-disclosure, #consent-panel, [data-consent], [data-mcp-consent]'
);
consentEls.forEach(consent => {
// Check previous siblings
let sibling = consent.previousElementSibling;
while (sibling) {
const cs = window.getComputedStyle(sibling);
const ba = cs.breakAfter;
if (ba && !['auto', 'avoid', ''].includes(ba)) {
console.error('SA-CSS-BKAF-001: break-after on element preceding consent', {
precedingEl: sibling,
breakAfter: ba,
consent
});
}
sibling = sibling.previousElementSibling;
}
// Check parent's previous siblings too
const parent = consent.parentElement;
if (parent) {
const ps = parent.previousElementSibling;
if (ps && !['auto', 'avoid', ''].includes(getComputedStyle(ps).breakAfter)) {
console.error('SA-CSS-BKAF-001: break-after on parent-preceding element', { ps });
}
}
});
}
Attack 2: break-after:column on penultimate sibling in multi-column — consent in off-screen column (SA-CSS-BKAF-002)
In a multi-column install dialog (column-count: 2), the element immediately before the consent disclosure applies break-after: column. This forces that element to fill the remainder of column 1, and the consent disclosure begins in column 2. If the column gap is set to a large value, column 2 starts off-screen. The consent element itself has no break-before or other fragmentation properties — only its sibling has break-after: column. An audit scanning only the consent element sees no suspicious fragmentation rules.
/* MCP attack: */
.install-options-container {
column-count: 2;
column-gap: 9999px; /* column 2 starts 9999px off-screen */
overflow: hidden;
}
.feature-list {
break-after: column; /* force feature list to fill column 1 entirely */
/* Column 1 end: feature list */
}
.consent-disclosure {
/* no break properties — audit of consent element passes */
/* Automatic column placement begins in column 2 — 9999px off-screen */
}
// Detection: check break-after on all siblings within multi-column containers
function detectColumnBreakAfterPattern() {
document.querySelectorAll('.consent-disclosure, [data-consent]').forEach(consent => {
const parent = consent.parentElement;
if (!parent) return;
const pcs = getComputedStyle(parent);
if (parseInt(pcs.columnCount) > 1 || parseFloat(pcs.columnWidth) > 0) {
// Inside multi-column: check all preceding siblings for break-after:column
let prev = consent.previousElementSibling;
while (prev) {
const ba = getComputedStyle(prev).breakAfter;
if (ba === 'column' || ba === 'always') {
const gap = parseFloat(pcs.columnGap);
if (gap > 100) {
console.error('SA-CSS-BKAF-002: break-after:column + large column-gap pushes consent off-screen', {
prev, consent, columnGap: gap
});
}
}
prev = prev.previousElementSibling;
}
}
});
}
Attack 3: @media print break-after:page on product section — consent orphaned in print output (SA-CSS-BKAF-003)
The product description section applies break-after: page only within a @media print block, so on screen there is no fragmentation effect. In the PDF output, the product section ends page 1 and consent begins page 2. The @media print scope means no JavaScript or computed-style check at screen-render time sees the break-after value — it only activates during the print rendering pass. The consent element has no print-specific styles; scanning it finds nothing. Only scanning the print media rule for break-after declarations near consent-adjacent elements reveals the attack.
/* MCP attack: */
@media print {
.product-description {
break-after: page;
/* Screen: no effect
Print: page break after product description
— consent is now on a separate page from install button */
}
/* The consent element has no @media print rules */
}
// Detection: scan @media print rules for break-after on consent-adjacent elements
function detectPrintMediaBreakAfter() {
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) {
const ba = inner.style?.breakAfter || inner.style?.getPropertyValue('break-after');
if (ba && !['auto', 'avoid', ''].includes(ba)) {
console.warn('SA-CSS-BKAF-003: break-after in @media print rule', {
selector: inner.selectorText,
breakAfter: ba
});
}
}
}
}
} catch (e) {}
}
}
Attack 4: JS beforeprint event injects break-after — reverted afterprint (SA-CSS-BKAF-004)
JS listens for window.beforeprint and adds break-after: page to the product summary element, then removes it in afterprint. During normal page inspection and at load time, no element has break-after set. The property only exists during the print rendering pass — invisible to static auditing, computed-style checks, and MutationObserver-based dynamic auditing (the observer never sees a change because the mutation occurs in a context where PDF capture is already in progress). The resulting PDF places consent on page 2 with no install button on the same page.
/* No suspicious CSS in the stylesheet */
// MCP JS — injects break-after only during print rendering:
window.addEventListener('beforeprint', () => {
const productSummary = document.querySelector('.product-summary');
if (productSummary) {
productSummary.dataset.origBreakAfter = productSummary.style.breakAfter || '';
productSummary.style.breakAfter = 'page';
/* PDF renderer captures this; consent lands on page 2 */
}
});
window.addEventListener('afterprint', () => {
const productSummary = document.querySelector('.product-summary');
if (productSummary) {
productSummary.style.breakAfter = productSummary.dataset.origBreakAfter || '';
}
});
// Detection: monitor for beforeprint event listener registrations (limited);
// Better approach: run headless Chrome print capture and verify page 1 contains consent
// The only reliable detection is print simulation, not computed style inspection
function detectBeforePrintInjection() {
// Intercept addEventListener to detect beforeprint/afterprint listeners
const origAddEventListener = window.addEventListener;
window.addEventListener = function(type, listener, options) {
if (type === 'beforeprint') {
console.warn('SA-CSS-BKAF-004: beforeprint event listener registered — possible print-time CSS injection');
}
return origAddEventListener.call(this, type, listener, options);
};
// Also: run print capture verification
// Use Playwright: await page.emulateMedia({ media: 'print' })
// Then check element positions at print viewport
}
Why break-after is harder to detect than break-before: Audits that scan the consent element's own computed style correctly find nothing — the attack is on adjacent elements. Detection requires: (1) checking breakAfter on all preceding siblings, parent, and parent-preceding elements; (2) scanning @media print rules for any break-after declaration; (3) running a headless-Chrome print simulation and verifying that the consent text appears on the same page as the install button. SkillAudit's audit engine performs all three checks as part of the fragmentation security module.
Attack summary
| ID | Property location | Consent element has suspicious CSS? | Severity |
|---|---|---|---|
| SA-CSS-BKAF-001 | Install button: break-after:page | No | High |
| SA-CSS-BKAF-002 | Preceding sibling: break-after:column + large column-gap | No | High |
| SA-CSS-BKAF-003 | @media print on product section | No | High |
| SA-CSS-BKAF-004 | JS beforeprint injection (transient) | No | High |
Consolidated finding blocks
break-after: page. In the PDF output, page 1 contains the install button; page 2 contains the consent disclosure. The consent element has no suspicious CSS properties — the attack is on its predecessor. Compliance audit records document the install action without adjacent consent. Detection requires checking breakAfter on all elements preceding the consent disclosure.
column-gap, the preceding sibling gets break-after: column, forcing it to end column 1. Consent auto-places into column 2 which begins off-screen. The consent element has no break properties — only its sibling does. Standard consent-element scanning finds nothing suspicious.
break-after: page inside a @media print block. Screen computed-style checks see no fragmentation properties. In the PDF output, consent lands on a separate page from the install button. Detection requires scanning all CSSMediaRule instances with print condition for break-after declarations.
beforeprint event listener adds break-after: page to a product summary element immediately before the PDF render; an afterprint listener removes it. No computed-style check at any other time reveals the property. Reliable detection requires headless Chrome print simulation with page-layout verification rather than CSS property inspection.
CSS break-before security | CSS break-inside security | Security Checklist