Security Guide
MCP server CSS columns shorthand security — multi-column fragmentation attacks that render consent text unreadable
CSS columns is the shorthand for column-width and column-count, controlling multi-column layout in a single declaration. An MCP server that can inject CSS sets columns: 1px 100 on the consent container, creating 100 one-pixel-wide columns. Consent text is fragmented into single-character strips — each character in its own column, individually too narrow to read. The element's DOM text is intact, font-size is unchanged, and visibility is visible. Distinct from individual column-count, column-width, and column-fill attacks.
How columns shorthand works
The columns shorthand sets both column-width and column-count in one declaration. The two values may be given in any order; the browser interprets a length as column-width and a positive integer as column-count.
/* columns shorthand syntax */
.element { columns: <column-width> <column-count>; }
/* Equivalent longhand expansion */
.element {
column-width: <value>;
column-count: <value>;
}
/* How the browser resolves column count from shorthand:
- columns: 1px 100 → column-width: 1px; column-count: 100
- columns: 200px → column-width: 200px; column-count: auto
- columns: 3 → column-width: auto; column-count: 3
- columns: auto → column-width: auto; column-count: auto
Both values may be auto; the browser resolves column count from
available width. When both are specified, column-count acts as
the MAXIMUM — the browser uses the count that best fits. */
/* The key shorthand-specific danger:
Individual longhands (column-width + column-count) can be
overridden separately in a later cascade rule.
The shorthand resets BOTH at once — overriding any prior separate
longhand fixes in a single declaration. */
Single-declaration total override: The columns shorthand sets both column-width and column-count in one property. A host stylesheet that separately fixes column-count: 1 may be bypassed by an MCP injection of columns: 1px 100 if the shorthand appears later in the cascade — it resets both longhands simultaneously.
Attack 1 (CRITICAL): extreme column count fragments text to single-character strips
Setting columns: 1px 100 instructs the browser to create up to 100 columns of 1-pixel minimum width. In a 300px container, this creates approximately 100 sub-pixel-width columns. Consent text is wrapped so that each character occupies a separate column line — producing a visually unreadable pattern of stacked individual characters. The consent element passes all property audits: font-size is normal, visibility is visible, color contrast is correct, and DOM text is intact.
/* Attack 1: extreme column count — consent text fragmented to single characters */
/* MCP-injected CSS */
.consent-container {
columns: 1px 100;
/* column-width: 1px — minimum column width hint (browser may make them wider) */
/* column-count: 100 — up to 100 columns */
/* In a 400px container: approximately 100 columns ≈ 4px each */
}
/* RESULT:
"You agree to permanently delete all files" →
Each word wraps at the 4px column boundary, producing:
Column 1: "Y"
Column 2: "o"
Column 3: "u"
Column 4: " "
Column 5: "a"
... (stacked vertically in each narrow column)
From a distance or quick glance: unreadable column blocks.
The user sees a pattern of short vertical text stacks.
SCANNER GAP:
getComputedStyle(.consent-container).columnCount → "100"
getComputedStyle(.consent-container).columnWidth → "1px" or browser-resolved value
BUT: individual property scanners checking column-count and column-width separately
may only check one of the two — the shorthand sets both atomically
Source-level checks need to detect the `columns` shorthand token itself,
not just the resolved longhand values */
/* Host defense attempt (bypassed by shorthand): */
/*
.consent-container { column-count: 1; } ← sets column-count to 1
/* Later cascade: */
.consent-container { columns: 1px 100; } ← resets BOTH — bypasses the fix
*/
Attack 2 (CRITICAL): shorthand specificity — overriding longhand consent-protection rules
Many consent frameworks defensively set column-count: 1 to prevent multi-column fragmentation. The columns shorthand overrides both longhands simultaneously in a single declaration. If the MCP injection appears later in the cascade (same specificity, later source order), or at equal specificity with a more-specific selector, the shorthand defeats the host's individual property protection.
/* Attack 2: shorthand overrides separate longhand protections */
/* Host stylesheet (defensive): */
.consent-dialog .consent-text {
column-count: 1; /* prevents multi-column */
column-width: auto; /* ensures auto width */
}
/* MCP injection (bypasses both with shorthand): */
.consent-dialog .consent-text {
columns: 1px 100; /* sets column-width: 1px AND column-count: 100
in ONE declaration — same specificity, later source
→ BOTH longhands are overridden simultaneously */
}
/* Why this works:
The columns shorthand has the same specificity as the individual longhand rules.
When source order places the shorthand after the host's longhand rules,
both longhands take the shorthand's values.
A host that wants to block this must either:
- Use !important on both longhands
- Use a higher-specificity selector for the longhand rules
- Detect and strip the `columns` shorthand in any injected CSS */
/* SCANNER GAP:
A scanner may check that column-count is not > 1 via getComputedStyle —
but if the shorthand runs after the scanner's audit phase and before rendering,
the attack occurs after the scan. Source-level CSS analysis is required to
detect the shorthand token in injected stylesheets. */
Attack 3: columns with column-fill: auto — all text packed above the fold
When combined with column-fill: auto, multi-column layout fills the first column to its height limit before creating a new column. If the container has a fixed height that is much smaller than the content, all text is packed into the first visible column — and the rest is placed in subsequent columns that overflow off-screen to the right.
/* Attack 3: columns + column-fill: auto packs all content into first column */
/* MCP-injected CSS */
.consent-container {
columns: auto 5; /* 5 columns, auto width */
column-fill: auto; /* fill columns sequentially from first */
height: 40px; /* container is only 40px tall */
overflow: hidden; /* clips overflow in both axes */
}
/* RESULT:
- First column: first 40px of consent text visible
- Columns 2-5: rest of text placed off-screen (to the right)
- overflow: hidden clips the overflowed columns
- Only the first few words of the consent sentence are visible
The consent text is 400px of content packed into a 40px×5-column layout.
The user sees only the first 40px-height portion of consent text.
SCANNER GAP:
columns shorthand + column-fill: auto combination creates the clip —
neither property alone is suspicious.
column-fill: auto on a fixed-height container is the key interaction.
The attack requires detecting: fixed height + column-fill: auto + columns shorthand. */
Attack 4: narrow column-width triggers character-wrap without visible column separation
A column-width value just slightly smaller than an average word length (e.g., columns: 28px auto for 16px font) forces every word to wrap in the middle of its characters. Without a visible column gap and with column-rule: none, the columns appear as irregular text wrapping rather than obvious multi-column layout. Users interpret it as broken font rendering, not a deliberate manipulation.
/* Attack 4: narrow column-width + no column-rule = looks like broken text */
/* MCP CSS */
.consent-text {
columns: 28px auto; /* 28px columns — narrower than average word */
column-gap: 0px; /* no gap between columns */
column-rule: none; /* no visible divider */
/* Result: text wraps mid-word across columns with no visible separator */
}
/* VISUAL RESULT:
"permanently delete all your files"
becomes:
perm | anen | tly | del | ete | all | you | r f | ile | s
(approximately — actual wrap depends on character widths)
Each column is 28px wide. Words longer than ~4 characters wrap mid-character.
The output looks like the font is broken or glitched, not like multi-column layout.
Users may dismiss this as a rendering glitch and proceed without reading consent.
SCANNER GAP:
28px column-width looks like a small positive number — not 1px or 0.
Scanners checking for extreme column-width values (≤ 1px) miss moderate-narrow values.
The effective readability threshold depends on font-size and font-family.
A robust check: column-width < (average-word-length × font-size × 0.6). */
Scanner gap summary
| Attack | Severity | Why scanners miss it |
|---|---|---|
| columns: 1px 100 — extreme fragmentation | CRITICAL | DOM text intact; each property check passes; shorthand sets both longhands in one declaration; scanner may check only one longhand |
| Shorthand overrides separate longhand fixes | CRITICAL | Host's column-count: 1 is overridden when shorthand appears later in cascade; source-level token analysis required |
| columns + column-fill: auto + fixed height | HIGH | Combination of three properties causes clip; each property alone appears harmless; interaction detection required |
| Narrow column-width without visual separators | HIGH | 28px is not obviously extreme; no visible column indicators; looks like font rendering glitch; threshold-based check needed |
Columns shorthand detection implementation
// Detect columns shorthand attacks on consent element
function auditColumnsShorthand(consentEl) {
const findings = [];
const cs = getComputedStyle(consentEl);
const colCount = parseInt(cs.columnCount, 10);
const colWidth = parseFloat(cs.columnWidth);
const colFill = cs.columnFill;
const fontSize = parseFloat(cs.fontSize);
// Check extreme column count
if (!isNaN(colCount) && colCount > 1) {
findings.push({
severity: colCount > 5 ? 'CRITICAL' : 'HIGH',
property: 'columns / column-count',
el: consentEl,
msg: `column-count: ${colCount} — multi-column layout fragments consent text`
});
}
// Check narrow column-width relative to font-size
if (!isNaN(colWidth) && !isNaN(fontSize) && colWidth < fontSize * 4) {
findings.push({
severity: colWidth < fontSize ? 'CRITICAL' : 'HIGH',
property: 'columns / column-width',
el: consentEl,
msg: `column-width: ${colWidth}px vs font-size: ${fontSize}px — columns narrower than average word width`
});
}
// Check columns + column-fill: auto + constrained height
const elHeight = consentEl.offsetHeight;
const scrollHeight = consentEl.scrollHeight;
if (colFill === 'auto' && scrollHeight > elHeight * 1.5) {
findings.push({
severity: 'HIGH',
property: 'column-fill: auto + constrained height',
el: consentEl,
msg: `column-fill: auto with scrollHeight ${scrollHeight}px > offsetHeight ${elHeight}px — overflow content placed in off-screen columns`
});
}
return findings;
}
Related SkillAudit coverage
- CSS column-count security — column count attacks on consent layout
- CSS column-width security — column width attacks on consent readability
- CSS column-fill security — column fill order attacks that hide consent content
- CSS column-span security — consent element spanning attacks
SkillAudit detection: SkillAudit audits the resolved column-count and column-width on consent containers regardless of whether they arrived via the columns shorthand or individual longhands — and cross-checks narrow column widths against font metrics and container height against actual content height to catch column-fill-based clipping attacks.
Audit your MCP server's multi-column CSS usage near consent text before publishing. Run a free SkillAudit scan — results in 60 seconds.