MCP server CSS column-rule-color security: transparent separator camouflage, background-matching, color-mix opacity, and column boundary removal attacks
Published 2026-09-25 — SkillAudit Research
The CSS column-rule-color property sets the color of the decorative rule drawn between columns in a multi-column container. In legitimate use, it provides a visual separator that helps users navigate between columns. In an adversarial MCP server consent UI, making this rule invisible removes the only visual signal that the consent dialog is multi-column — allowing the attacker to hide consent content in adjacent columns that users do not know exist.
column-rule-color is a longhand of the column-rule shorthand (alongside column-rule-style and column-rule-width). It follows the same color value syntax as border-color, accepting named colors, hex, rgb(), hsl(), and the color-mix() function.
Dependency on column-rule-style: A column rule is only painted if column-rule-style is not none (the default). Setting column-rule-style: solid and then column-rule-color: transparent creates a painted-but-invisible rule. If the host CSS already sets a visible rule (common in UI frameworks), overriding only column-rule-color to transparent is sufficient to remove the separator signal. See: CSS column-rule-style security.
Attack findings
Setting
column-rule-color: transparent with column-rule-style: solid paints a rule with zero opacity. The column boundary signal is removed. Users viewing a two-column consent dialog where the second column contains the consent clause see only the first column's content (e.g., a feature list or intro text) without any visual cue that a second column exists. The consent clause in the second column is present in the DOM and renders normally — it is simply not discoverable by a user who has no reason to scroll horizontally.
/* Host stylesheet already sets a visible separator */
.consent-modal { column-count: 2; column-rule: 1px solid #ccc; }
/* MCP injection overrides only the color longhand */
.consent-modal { column-rule-color: transparent; }
/* Result: rule painted at 1px solid but with opacity 0 — invisible.
User sees two columns as one undivided block.
Consent clause in column 2 is never discovered. */
Instead of
transparent, the attacker sets column-rule-color to the exact background color of the consent container (#ffffff, #0a0a0a, or whatever the host UI uses). The rule is painted but camouflaged against the background. This passes any check that looks for transparent specifically — the rule has a fully opaque, non-transparent color. It simply happens to be identical to the background. Static audits that check "is column-rule-color transparent?" miss this variant.
.consent-modal {
background: #1a1a2e; /* dark theme background */
column-count: 2;
column-rule-style: solid;
column-rule-width: 2px;
column-rule-color: #1a1a2e; /* identical to background — invisible */
}
The
color-mix() function can produce zero-opacity colors: color-mix(in srgb, transparent 100%, black 0%) evaluates to fully transparent. More subtly, color-mix(in srgb, #ccc 0%, transparent 100%) also produces transparent. A static audit that checks column-rule-color for the literal keyword transparent misses these computed-transparent values. The rule is visually identical to a transparent rule but the property value is a color-mix() expression.
.consent-modal {
column-rule-style: solid;
column-rule-width: 1px;
/* Computed value: transparent — but stored as color-mix() expression */
column-rule-color: color-mix(in srgb, transparent 100%, #fff 0%);
}
Combining
column-gap: 0 (removes the gutter between columns) with column-rule-color: transparent (removes the separator line) makes adjacent columns appear as a single continuous block of text. In a two-column consent layout where the first column contains a short header and the second contains the consent clause, the user sees the entire container as a single-column layout. The consent clause is present immediately below the header in the visual flow — but it is actually in a separate column, and only the first column is within the visible viewport width if the container is narrow enough to clip column 2.
Detection
function checkColumnRuleColor(container) {
const cs = getComputedStyle(container);
// Only relevant if column-rule-style is not 'none'
if (cs.columnRuleStyle === 'none') return null;
const color = cs.columnRuleColor;
// Check for transparent keyword (computed value)
if (color === 'transparent' || color === 'rgba(0, 0, 0, 0)') {
return { severity: 'high', issue: 'column-rule-color resolves to transparent' };
}
// Check for background-matching camouflage
const bg = cs.backgroundColor;
if (bg !== 'transparent' && bg !== 'rgba(0, 0, 0, 0)' && color === bg) {
return { severity: 'high', issue: 'column-rule-color matches background color' };
}
// Check computed opacity via parsed rgba alpha channel
const alphaMatch = color.match(/rgba\(\s*\d+,\s*\d+,\s*\d+,\s*([\d.]+)\s*\)/);
if (alphaMatch && parseFloat(alphaMatch[1]) < 0.05) {
return { severity: 'medium', issue: 'column-rule-color has near-zero alpha' };
}
return null;
}
Remediation
| Control | How it helps |
|---|---|
| Audit computed column-rule-color against background | Catches background-matching camouflage that literal transparency checks miss |
| Resolve color-mix() at runtime via getComputedStyle() | Computed value after color-mix() resolution is the actual rendered color; check that, not the raw CSS expression |
| Restrict column-count to 1 in sandboxed consent UIs | Eliminates the entire column-rule attack surface; consent rendered in a single column has no separators to hide |
| Pair with column-gap audit | Zero gap + transparent rule together indicate column-merging intent; flag both together as compounded risk |
SkillAudit checks column-rule-color computed values against background colors at runtime, resolves color-mix() expressions, and flags multi-column containers where the separator signal has been removed. Run a free audit on any MCP server GitHub URL.