MCP server CSS column-rule-style security: hidden separators, none overrides, groove/ridge optical illusions, and column-count invisibility attacks
Published 2026-09-25 — SkillAudit Research
The CSS column-rule-style property sets the line style of the rule drawn between columns in a multi-column layout. Its initial value is none — meaning no rule is drawn by default. When a host UI framework sets a visible rule (e.g., column-rule: 1px solid #ddd), an MCP server injecting CSS can override only the style longhand to none or hidden, removing the separator signal without changing the shorthand's other parameters.
column-rule-style accepts the same values as border-style: none, hidden, solid, dashed, dotted, double, groove, ridge, inset, outset. The distinction between none and hidden matters: hidden suppresses the rule even in border-collapse table contexts and has higher specificity implications in some layout systems.
Attack findings
When the host UI framework renders a visible column separator (common in dialog and wizard components), setting
column-rule-style: hidden removes it. The multi-column layout remains active — columns still exist, content still flows into them — but there is no visual indicator that more than one column is present. A consent clause in column 2 is present in the DOM but gives no visual cue of its existence to a user who only sees column 1's content.
/* Host framework renders a separator */
.consent-wizard { column-count: 2; column-rule: 1px solid var(--border-color); }
/* MCP injection removes the separator via style longhand override */
.consent-wizard { column-rule-style: hidden; }
/* column-count:2 still active. Consent in column 2 is invisible.
No visual separator to alert user that column 2 exists. */
The
none value is functionally identical to hidden for column rules in non-table contexts. It is the initial value, which means setting it explicitly looks like a reset to defaults rather than a deliberate attack. A static audit that only flags rules explicitly setting hidden will miss this. The result is identical: a host-set visible separator is overridden to nothing. The multi-column container appears to have no columns.
The
groove and ridge values render a 3D-effect rule using two sub-pixels of complementary light/dark tones. On narrow consent columns (<80px wide), the groove/ridge rule visually merges with the text edges on both sides of the separator. Users perceive the column separator as part of the text content rather than as a boundary. This is not a full invisibility attack — the separator is present — but it significantly reduces the visual clarity of column boundaries, making it harder for users to recognize they are reading a multi-column layout. Combined with other column displacement attacks, groove/ridge rules degrade the user's ability to notice that content continues beyond the visible area.
.consent-modal {
column-count: 3;
column-width: 60px; /* narrow columns */
column-rule-style: groove; /* 3D effect merges with text edges */
column-rule-width: 1px;
column-rule-color: #888;
overflow: hidden; /* clips column 3+ */
}
CSS cascade order means a longhand property set after a shorthand overrides only that specific component. An MCP server can inject a block that first sets a benign-looking shorthand and then overrides the style longhand:
column-rule: 1px solid black; column-rule-style: none;. A static scanner that reads only the shorthand reports "column-rule: solid — visible separator present." The subsequent longhand sets the style to none, removing the separator. Scanners that do not simulate CSS cascade order will report a false-negative.
.consent-modal {
/* Scanner reads this: solid separator — looks safe */
column-rule: 2px solid #333;
/* Longhand after shorthand overrides the style component */
column-rule-style: none; /* actual rendered state: no rule */
}
Detection
function checkColumnRuleStyle(container) {
const cs = getComputedStyle(container);
// Computed columnRuleStyle reflects the cascade-resolved value
// (longhand wins over shorthand when set after)
const style = cs.columnRuleStyle;
const cc = parseInt(cs.columnCount, 10);
if (cc <= 1) return null; // single column — no separator relevant
if (style === 'none' || style === 'hidden') {
// Multi-column container with no visible separator
// Check whether column-count suggests intentional multi-column layout
if (cc >= 2) {
return {
severity: 'high',
issue: `column-rule-style:${style} on ${cc}-column container — separator signal removed`
};
}
}
// Groove/ridge on narrow columns
if ((style === 'groove' || style === 'ridge')) {
const colW = container.getBoundingClientRect().width / cc;
if (colW < 100) {
return {
severity: 'medium',
issue: `column-rule-style:${style} on narrow ${Math.round(colW)}px columns — separator legibility degraded`
};
}
}
return null;
}
Remediation
| Control | How it helps |
|---|---|
| Audit computed column-rule-style (not raw CSS) | Computed value reflects longhand-after-shorthand cascade; raw CSS parsing misses override order |
| Flag multi-column containers with style:none or style:hidden | Direct indicator of separator-signal removal in a multi-column consent context |
| Restrict column-count to 1 in sandboxed consent UIs | Eliminates the entire column-rule attack surface at the source |
| Pair with column-rule-color audit | Both longhands can independently hide the separator; check both |
SkillAudit resolves cascade order for all column-rule longhands at runtime, flagging cases where a shorthand is followed by a hiding longhand. Run a free audit on any MCP server GitHub URL.