Security Guide

MCP server CSS columns consent security — multi-column fragmentation, off-screen second column, forced column breaks push consent out of view

CSS multi-column layout (columns, column-count, column-fill) fragments inline content into multiple columns side-by-side. When a consent container has a restricted height and overflow: hidden, text fills the first column and overflows into a second column that extends beyond the container's clip boundary — the second half of the consent text is invisible. column-break-after: always after the opening sentence forces all remaining consent into the invisible second column. All consent text is present in the DOM and passes opacity, visibility, and color checks.

How CSS multi-column layout enables consent fragmentation attacks

The CSS multi-column layout model causes inline content to flow from one column to the next, left-to-right (in LTR documents). Column height is determined by the container height when column-fill: auto is set — the browser fills each column to the container's block-size before flowing to the next column. If the container has overflow: hidden and the columns extend horizontally beyond the container's width, or if the container clips at a height that cuts off the second column, content in that second column is hidden from view.

Unlike height clipping (where scrollHeight > clientHeight signals truncation), multi-column clipping produces columns that are simply not rendered within the container's visible area. The content is in the DOM, fully laid out, and readable by screen readers — but visually absent for sighted users on the page.

Attack 1: columns:2 + height restriction fragments consent (SA-CSS-COL-001)

The consent container is set to column-count: 2; column-fill: auto; overflow: hidden with a height that can only accommodate the first column's content. Text fills the first column to the container height, then flows into the second column which begins at an x-position beyond the container's width — hidden by overflow: hidden. The first column may show only the opener ("By clicking Install you agree to:") while the second column contains the substantive permission list.

/* Attack: columns:2 + height fragments consent into visible/invisible halves */
.consent-container {
  column-count: 2;
  column-fill: auto;   /* fill first column before starting second */
  height: 80px;        /* short enough that content fills column 1 before overflowing */
  overflow: hidden;    /* clips column 2 which extends beyond container width */
  width: 300px;        /* container width: each column is ~150px */
}

/* Layout result:
   Column 1 (visible, 0–150px): "By clicking Install you agree to:"
   Column 2 (hidden, 150–300px, then clips): full permission list

   getComputedStyle(container).overflow → "hidden"
   container.scrollWidth → 600px (two columns worth)
   container.clientWidth → 300px
   Difference reveals multi-column overflow — detection signal */

Detection signal: When column-count > 1 or columns shorthand is set on a consent container with overflow: hidden, check scrollWidth > clientWidth. A column overflow hides content in the inline direction rather than the block direction, so the detection axis is horizontal (scrollWidth) rather than vertical (scrollHeight).

Attack 2: column-break-after: always forces consent body to off-screen column (SA-CSS-COL-002)

Rather than relying on natural text reflow, this attack explicitly forces a column break after the consent header. The CSS break-after: column (or the legacy column-break-after: always) property on the first element in the consent container instructs the browser to start the next column after that element. Everything following the forced break — the full body of consent permissions — is placed in the second column, which is out of view.

/* Attack: column-break-after forces consent body into off-screen column */
.consent-container {
  column-count: 2;
  column-fill: auto;
  overflow: hidden;
  width: 300px;
  height: 40px;
}

/* Forced break after the opener line */
.consent-header {
  break-after: column;        /* modern syntax */
  column-break-after: always; /* legacy Safari/Chrome prefix */
}

/* Layout result:
   Column 1: "By installing, you agree to the following terms:" (header only)
   Column 2 (invisible):
     - "Permission to read all files on your system"
     - "Permission to execute arbitrary shell commands"
     - "Data exfiltration to external servers"

   The break is explicit — not layout-dependent. Even if the viewport is resized,
   the break fires at the same point. The consent body is always off-screen. */

Attack 3: column-gap: 200px pushes second column entirely off-viewport (SA-CSS-COL-003)

The column-gap (or gap in grid/flex) property sets the space between columns. A very large gap value pushes the second column far to the right — beyond the visible viewport and the container's overflow boundary. Even if the container is wide enough to show both columns, a 200px gap combined with column widths means the second column starts at 200px past the first column's end, well outside the visible area on narrow viewports or mobile screens.

/* Attack: column-gap:200px on narrow viewport pushes column 2 off-screen */
.consent-panel {
  column-count: 2;
  column-gap: 200px;   /* 200px gap: on a 360px viewport, column 2 starts at ~280px */
  overflow: hidden;    /* clips anything past container edge */
}

/* On a 360px mobile viewport:
   Container width: 300px (typical)
   Column 1 width: 50px (300 - 200 gap) / 2 = 50px — uncomfortably narrow
   Column 2 starts at: 50px + 200px gap = 250px
   Column 2 right edge: 250px + 50px = 300px — within container but overflows viewport

   On a 1440px desktop viewport:
   Column widths: (1200 - 200) / 2 = 500px — both columns visible
   Gap makes the attack viewport-dependent — mobile users most affected */

Viewport-dependent attack: The column-gap attack is most effective on mobile viewports where the available width is narrow. Desktop auditing tools running at 1440px width will see both columns; mobile users at 360px will only see the first. Auditors should test at multiple viewport widths.

Attack 4: orphans: 999 collapses consent to single character in first column (SA-CSS-COL-004)

The CSS orphans property controls the minimum number of lines that must remain in a paragraph before a column or page break. Setting orphans: 999 on consent text paragraphs instructs the browser to try to keep 999 lines together before allowing a break — an impossible requirement for any real paragraph. The browser resolves this by placing the entire paragraph into the next column to avoid orphaning, resulting in all consent text flowing to the second (invisible) column. The first column shows only the outermost container element with no readable content.

/* Attack: orphans:999 forces entire consent paragraphs into second column */
.consent-container {
  column-count: 2;
  column-fill: auto;
  overflow: hidden;
  height: 60px;
}

.consent-text p {
  orphans: 999;   /* try to keep 999 lines together before column break */
}

/* Result: the browser cannot satisfy the orphans constraint in column 1
   (not enough height). It moves entire paragraphs to column 2 to avoid violating
   the orphans constraint. Column 1 may show only the heading or be empty.
   Column 2 (off-screen) contains all consent paragraphs.

   The orphans property is rarely audited — it has no obvious consent-hiding
   use case and is typically ignored by consent-focused security scanners. */

Findings summary

HIGH SA-CSS-COL-001: columns:2 + overflow:hidden fragments consent into visible/invisible halves — scrollWidth > clientWidth reveals off-screen second column
HIGH SA-CSS-COL-002: break-after:column / column-break-after:always after consent header forces full permission list into off-screen second column
HIGH SA-CSS-COL-003: column-gap:200px pushes second column off-viewport on narrow screens — mobile-targeted fragmentation attack
MEDIUM SA-CSS-COL-004: orphans:999 on consent paragraphs forces entire paragraphs into second column to satisfy impossible orphan constraint

Defences

Detect column-count > 1 on consent containers: Compute styles on consent containers and flag any element with column-count > 1, columns shorthand, or column-width set. Multi-column layout on consent containers is rarely legitimate and frequently a fragmentation attack vector.

Check scrollWidth vs clientWidth: On consent containers with multi-column layout, a scrollWidth significantly exceeding clientWidth indicates that one or more columns overflow the visible area. Combined with overflow: hidden, this signals that column content is clipped.

Flag break-after: column on consent child elements: Enumerate child elements of consent containers and check computed break-after values. A forced column break on any child element inside a consent container should be flagged as a potential fragmentation attack.

Check orphans values on consent text: Unreasonably large orphans values (>10) on consent paragraphs in multi-column containers should trigger a finding — they indicate an intent to force content to the next column.

Related: CSS @counter-style consent security · CSS scroll-snap consent security · CSS object-fit consent security