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

HIGH
column-rule-style: hidden — rule suppression with host separator override
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. */
HIGH
column-rule-style: none — initial value restore to undo host separator
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.
MEDIUM
column-rule-style: groove or ridge on narrow consent columns — optical illusion separator merger
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+ */
}
MEDIUM
column-rule-style longhand targeting after shorthand — override evasion
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

ControlHow 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:hiddenDirect indicator of separator-signal removal in a multi-column consent context
Restrict column-count to 1 in sandboxed consent UIsEliminates the entire column-rule attack surface at the source
Pair with column-rule-color auditBoth 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.