Security Guide
MCP server CSS @counter-style consent security — invisible marker symbols, zero-width counters, and checkmark pre-agreement deception
CSS @counter-style defines the symbol rendered as the list marker for ordered and unordered list items. By specifying invisible Unicode characters — U+00AD soft-hyphen or U+200B zero-width space — as the counter symbol, an attacker removes list markers from consent items without removing the consent text. A checkmark variant assigns a ✓ symbol to every consent list item, creating the false impression that the user has already agreed to each item. The consent text remains fully present in the DOM and passes all visibility checks.
How @counter-style enables consent marker attacks
The @counter-style at-rule allows authors to define custom counter systems beyond the built-in disc, decimal, lower-alpha, and similar types. The symbols descriptor specifies the glyphs or strings used as list markers. When a symbol is a Unicode character that renders with zero visual width — such as U+00AD (soft-hyphen) or U+200B (zero-width space) — the resulting list marker occupies space in the layout but produces no visible glyph. To a user, the list appears to have no markers; to an auditor checking list-style-type or list-style shorthand, the value appears to be a valid custom style rather than none.
This attack family is distinct from list-style: none because the marker is technically present and rendered — just with an invisible glyph. Automated checks that detect list-style: none on consent lists will not flag a custom counter-style using a zero-width symbol.
Attack 1: @counter-style with U+00AD soft-hyphen symbol (SA-CSS-CS-001)
The soft-hyphen character (U+00AD) is a conditional hyphen: it is visible only at line-break opportunities where the browser chooses to hyphenate. When used as a counter symbol, the browser renders the soft-hyphen as the list marker, but because it is not at a line-break point, the character is invisible. The consent list items appear to have no markers. The list-style-type property returns a computed value referencing the custom counter-style name, not none.
/* Attack: @counter-style using U+00AD soft-hyphen as symbol */
@counter-style hidden-marker {
system: cyclic;
symbols: "\00AD"; /* U+00AD: soft-hyphen — renders invisibly */
suffix: " ";
}
.consent-list {
list-style: hidden-marker;
}
/* getComputedStyle(li).listStyleType → "hidden-marker" (not "none")
Marker is present in the DOM accessibility tree
Marker glyph is invisible — no visual indicator of list items
Detection: enumerate @counter-style rules and check symbols for zero-width Unicode */
Detection note: Standard auditor checks for list-style: none will not flag this pattern. Detection requires parsing @counter-style rules from the document's stylesheets and checking whether the symbols descriptor contains zero-width or invisible Unicode code points (U+00AD, U+200B, U+FEFF, U+200C, U+200D, etc.).
Attack 2: @counter-style with U+200B zero-width space symbol (SA-CSS-CS-002)
The zero-width space (U+200B) is a Unicode character with an advance width of zero. Unlike soft-hyphen, it has no conditional rendering — it is always invisible. Used as a counter symbol, it produces a list marker with no visual presence and no layout impact (no indentation gap where the marker would appear). The absence of the usual indentation for list markers may itself be a signal, but in a densely styled MCP install UI, the layout difference is unlikely to be noticed by a user.
/* Attack: @counter-style using U+200B zero-width space */
@counter-style zero-marker {
system: cyclic;
symbols: "\200B"; /* U+200B: zero-width space — no width, no glyph */
suffix: "";
}
.consent-items {
list-style: zero-marker;
padding-left: 0; /* removes indent — layout looks like a plain div stack */
}
/* Result: consent list renders as a stack of plain text lines
No bullets, no numbers, no visual indication of list structure
list-style-type computed: "zero-marker" — not caught by list-style:none detectors
padding-left:0 removes the standard list indentation visual cue */
Attack 3: @counter-style checkmark symbol implies all items are pre-agreed (SA-CSS-CS-003)
This attack changes the semantic framing of consent list items rather than hiding the markers. By assigning a checkmark glyph (✓ U+2713 or ✔ U+2714) as the counter symbol, every consent list item appears with a green or neutral checkmark marker. Users who scan the list visually may interpret the checkmarks as confirmation that the items are pre-approved, already agreed to, or optional rather than required. This pattern exploits the learned visual association between checkmarks and "completed" or "accepted" states.
/* Attack: @counter-style checkmark implies all items are pre-agreed */
@counter-style pre-agreed {
system: cyclic;
symbols: "✓"; /* U+2713: checkmark — implies agreement */
suffix: " ";
}
.consent-list {
list-style: pre-agreed;
color: var(--text);
}
/* Visual result: each consent item begins with "✓ Install grants access to..."
User perception: "these are items I've already agreed to" or "these are optional"
Actual meaning: these are required permissions being granted
Detection: check @counter-style symbols for confirmation/positive glyphs (✓ ✔ ✅) on consent lists */
Semantic deception: This attack does not hide consent — it reframes it. The checkmarks signal completion or approval, conditioning users to interpret the list as a summary of choices already made rather than terms requiring consideration. The consent text is fully readable; only its framing is manipulated.
Attack 4: list-style: none + ::before pseudo-element removal (SA-CSS-CS-004)
A variant that combines explicit list-style: none with the removal of any ::before pseudo-element content on list items. Some consent UIs add custom markers via ::before { content: "•" } as an alternative to list-style. An attacker can suppress both: set list-style: none to remove native markers, then override any ::before content to content: "" or content: "\200B". The result is a visually marker-free list where neither the native nor the custom markers render.
/* Attack: strip native markers + suppress ::before custom markers */
.consent-list {
list-style: none;
padding-left: 0;
}
.consent-list li::before {
content: "\200B"; /* override any custom bullet added by base styles */
display: inline;
width: 0;
margin: 0;
}
/* If base styles defined: .consent-list li::before { content: "→"; color: var(--accent); }
This override replaces the visible arrow with an invisible zero-width space
The ::before pseudo-element is still present in computed styles — harder to flag
Detection: check computed content of ::before on consent list items for invisible characters */
Findings summary
Defences
Enumerate @counter-style rules and inspect symbols: Parse all @counter-style at-rules from document stylesheets. For each rule applied to a consent-containing list, inspect the symbols descriptor for zero-width Unicode characters (U+00AD, U+200B, U+FEFF, U+200C, U+200D, U+00A0 non-breaking space at zero advance, and similar). Flag any custom counter-style that produces no visible glyph on consent lists.
Detect semantic deception via positive glyphs: Flag @counter-style rules on consent lists whose symbols include confirmation characters (U+2713 ✓, U+2714 ✔, U+2705 ✅, U+2611 ☑). These may indicate a pre-agreement framing attack.
Check ::before computed content on consent list items: Enumerate consent list items and check getComputedStyle(li, '::before').content. Flag invisible content values (empty string, zero-width space, soft-hyphen) that override visible custom markers.
Related: CSS columns consent security · CSS resize consent security · CSS scroll-snap consent security