MCP server CSS @counter-style additive security: additive system consent step misrepresentation, cyclic looping, symbolic marker obscuring, and fixed marker overflow attacks

Published 2026-09-25 — SkillAudit Research

The CSS @counter-style at-rule allows authors to define custom list marker systems. The system descriptor selects the algorithm used to convert counter values to marker strings. Three system values — additive, cyclic, and symbolic — have particular implications for MCP server consent UI security:

For MCP servers that render multi-step consent wizards — "Step 1: Data collection", "Step 2: Third-party sharing", "Step 3: Acceptance" — a custom @counter-style applied to the ordered list can replace meaningful step numbers with misleading, opaque, or cyclic markers that prevent users from understanding which step they are on or how many steps remain.

Critical consent flow attack: A consent wizard with 7 steps numbered "Step 1" through "Step 7" is a material disclosure to the user — they know they have not finished reading. A system: cyclic counter with 3 symbols displays steps 4, 5, 6, 7 as "Step 1", "Step 2", "Step 3" again — the cycle repeats. The user believes they are back at the beginning and has already read everything, when in fact steps 4-7 contain the critical data-sharing and liability clauses.

Attack 1: system:additive with homoglyph symbols — numeric step misrepresentation

The additive system maps counter values to marker strings by summing symbol weights. An adversary defines additive symbols that look like standard Western Arabic numerals but use Unicode homoglyphs or visually similar characters. Step 3 renders as a character that appears to be "3" but is not — it may be a fullwidth digit (U+FF13 3), a mathematical digit, or a Unicode number form.

/* Homoglyph numeric counter via additive system */
@counter-style consent-steps {
  system: additive;
  additive-symbols:
    9 "9",   /* U+FF19 FULLWIDTH DIGIT NINE */
    8 "8",   /* U+FF18 */
    7 "7",   /* U+FF17 */
    6 "6",   /* U+FF16 */
    5 "5",   /* U+FF15 */
    4 "4",   /* U+FF14 */
    3 "3",   /* U+FF13 */
    2 "2",   /* U+FF12 */
    1 "1";   /* U+FF11 FULLWIDTH DIGIT ONE */
  /* Step 1 renders as: "1." (fullwidth)
     To most users, "1" and "1" are visually identical at normal font sizes.
     The DOM ::marker pseudo-element textContent = "1."
     An audit comparing marker textContent to expected "1." will fail the match
     depending on whether it uses === or normalized comparison.

     Practical attack: the fallback counter on step 10 (not in additive-symbols
     for this 9-symbol set) falls back to the 'fallback' descriptor value.
     If fallback is 'disc', step 10 renders as a bullet.
     The user sees: 1, 2, 3, 4, 5, 6, 7, 8, 9, •
     They may miss that the final consent step shows as a bullet instead of "10". */
  suffix: ". ";
  fallback: disc;
}

/* Applied to the consent wizard list */
ol.consent-wizard { list-style: consent-steps; }

/* Detection: compare the @counter-style name used on ordered lists;
   extract additive-symbols and check for non-ASCII or homoglyph characters */
function detectHomoglyphCounter(styleSheets) {
  const findings = [];
  for (const sheet of styleSheets) {
    for (const rule of sheet.cssRules || []) {
      if (rule.type !== CSSRule.COUNTER_STYLE_RULE) continue;
      const text = rule.cssText;
      if (/system\s*:\s*additive/.test(text)) {
        const symbols = text.match(/additive-symbols\s*:[^;]+/);
        if (symbols) {
          const hasNonAscii = /[^\x00-\x7F]/.test(symbols[0]);
          if (hasNonAscii) {
            findings.push({ ruleName: rule.name, text: text.slice(0, 200),
              note: 'additive @counter-style with non-ASCII symbols — potential homoglyph misrepresentation' });
          }
        }
      }
    }
  }
  return findings;
}

Attack 2: system:cyclic — consent step looping

A cyclic counter cycles through its symbols indefinitely. Step N is represented by symbol at index (N-1) mod (number of symbols). An adversary defines a cyclic counter with 3 symbols for a 7-step consent flow. Steps 1, 2, 3 show as expected. Steps 4, 5, 6 show the cycle symbols again — appearing identical to steps 1, 2, 3. Step 7 shows as step 1 again. The user perceives the wizard as having only 3 steps and believes they have already reviewed the content of "step 1" when they see step 4.

/* Cyclic counter that repeats every 3 steps */
@counter-style misleading-steps {
  system: cyclic;
  symbols: "1" "2" "3";
  /* A 7-step consent flow renders as:
     Step 1: "1. Data collection"       → user reads
     Step 2: "2. Third-party sharing"   → user reads
     Step 3: "3. Arbitration clause"    → user reads
     Step 4: "1. Liability limitation"  → user thinks "I've seen step 1"
     Step 5: "2. Auto-renewal terms"    → user thinks "already read"
     Step 6: "3. Data retention (5yr)"  → user thinks "already read"
     Step 7: "1. I accept all terms"    → user thinks "already read"
     The user accepts at step 7 without reading steps 4-7.

     Note: if the host renders "Step X of Y" text from JS, the cyclic counter
     affects only the marker. The "X of Y" label from JS shows 4/7, 5/7, etc.
     This attack is most effective when no JS-driven step label exists — only
     the CSS-rendered ::marker is the step indicator. */
  suffix: ". ";
}

/* Subtler variant: use Unicode circled numbers that look like step indicators */
@counter-style circled-steps {
  system: cyclic;
  symbols: "①" "②" "③";
  /* Circled numbers ①②③ are standard consent UI patterns in some locales.
     A cyclic counter with these three symbols creates a visually
     "correct-looking" consent flow for any number of steps.
     The user sees ①②③①②③ and may not notice the repetition. */
  suffix: " ";
  fallback: decimal;
}

Attack 3: system:symbolic — exponential marker growth obscuring labels

A symbolic counter repeats its symbols with increasing multiplicity: step 1 = ●, step 2 = ●●, step 3 = ●●●. For a large step number (e.g., step 20 in a long consent flow), the marker is ●●●●●●●●●●●●●●●●●●●●. This 20-character marker may overflow the list-item marker box. Depending on list-style-position and the container's overflow setting, the long marker can:

/* Symbolic counter with exponential growth */
@counter-style long-markers {
  system: symbolic;
  symbols: "●";
  /* Step 1: "●"       (1 char)
     Step 5: "●●●●●"  (5 chars)
     Step 10: 10 bullet chars (~20px at 14px font)
     Step 20: 20 bullet chars (~40px — often wider than marker box)

     With list-style-position: inside, the markers are inline with content.
     A 40px wide marker at a 14px font-size indents the first line 40px.
     The consent text on the first line is shifted 40px right.
     If the container is 200px wide: remaining content width = 160px.
     This is fine for step 20.

     For a single-symbol symbolic counter on a list with 100 items,
     step 100's marker is 100 symbols ≈ 200px — the full container width.
     The consent text on the first line is completely obscured by the marker. */
  suffix: " ";
}

/* overflow: hidden on the list clips wide markers AND the start of content */
ol.terms-list {
  list-style: long-markers inside;
  overflow: hidden;
  width: 300px;
  /* At step 30: marker = 30 bullet chars ≈ 60px.
     Content: 240px remaining.
     At step 300: marker = 300 chars ≈ 600px > container width.
     The marker itself overflows. With overflow:hidden, the consent text
     start is clipped — the first words of each consent item are invisible. */
}

/* Detection: check for symbolic @counter-style with single or few symbols
   applied to lists with many items */
function detectSymbolicMarkerGrowth(root, styleSheets) {
  const symbolicCounters = new Set();
  for (const sheet of styleSheets) {
    for (const rule of sheet.cssRules || []) {
      if (rule.type !== CSSRule.COUNTER_STYLE_RULE) continue;
      if (/system\s*:\s*symbolic/.test(rule.cssText)) {
        symbolicCounters.add(rule.name);
      }
    }
  }
  const findings = [];
  root.querySelectorAll('ol, ul').forEach(list => {
    const cs = window.getComputedStyle(list);
    const lsType = cs.listStyleType;
    if (symbolicCounters.has(lsType)) {
      const itemCount = list.querySelectorAll('li').length;
      if (itemCount > 10) {
        findings.push({ list, listStyleType: lsType, itemCount,
          note: `symbolic @counter-style on list with ${itemCount} items — marker width grows linearly; items beyond ~20 may have overflow` });
      }
    }
  });
  return findings;
}

Attack 4: fixed @counter-style with wide additive symbols — marker overflow truncation

An additive counter can define symbols that are individually wide — multi-character strings, emoji, or Unicode sequences. Setting additive-symbols: 1 "✓ Step" creates a marker string "✓ Step" for the value 1 — a 6-character wide marker rendered before the consent text. If the marker box is not wide enough, the text is truncated. Combined with the pad descriptor, the attacker can force the marker to a fixed minimum width that is wider than the container allows for consent text.

/* Wide additive symbols with pad */
@counter-style wide-steps {
  system: additive;
  additive-symbols:
    3 "Step Three Complete — ",
    2 "Step Two Complete — ",
    1 "Step One Complete — ";
  pad: 30 " ";
  /* Each marker is "Step N Complete — " (18+ characters).
     With list-style-position:inside and a 200px wide list,
     the 18-char marker takes ~150px, leaving ~50px for consent text.
     50px at 14px font = about 4-5 characters per line.
     Consent text is broken into 4-char fragments across many lines,
     becoming extremely hard to read.

     With list-style-position:outside (default):
     The marker is in the margin (outside the content box).
     A very wide outside marker may overlap the content if the
     list has insufficient left-padding to accommodate it.
     Default browser UA stylesheets typically give ol 40px left-padding.
     A 150px marker overflows the 40px margin into the content box. */
  suffix: "";
}

/* Exploit: zero-width space in additive symbols creates invisible markers
   while making the marker box non-zero width */
@counter-style invisible-step {
  system: additive;
  additive-symbols:
    1 "\200B"; /* U+200B ZERO WIDTH SPACE — renders as empty, but present */
  /* Marker appears empty (zero-width space is invisible).
     The list appears to have no markers — no "Step 1.", "Step 2." guidance.
     The user sees a plain list of paragraphs without step numbering.
     If the consent flow relies on step markers to communicate "you must
     read all N steps before accepting", the invisible markers remove
     this UI signal entirely. */
}

Summary

AttackMechanismSeverityDetection method
HIGHadditive system with homoglyph digits
Fullwidth or Unicode homoglyphs replace standard numerals; visually identical to users, fail string equality in audits Step numbers appear correct visually; programmatic audit misses the substitution; fallback symbol on step 10+ may be bullet Check additive-symbols for non-ASCII characters; normalize Unicode before comparing to expected step markers
CRITICALcyclic system — consent step looping
N-symbol cycle repeats; steps > N appear identical to steps 1–N; user believes they have already read the content Later consent steps rendered as step 1, 2, 3 again; user accepts without reading data-sharing and liability clauses Flag system:cyclic on any @counter-style used in consent flows; verify step count matches symbol count
MEDIUMsymbolic system — exponential marker growth
Single-symbol symbolic counter generates N-char marker for step N; wide markers overflow into consent text At high step counts, marker width exceeds container; consent text clipped or shifted off-screen by overflow:hidden Check symbolic @counter-style on lists with > 10 items; compute expected marker width at max step count
MEDIUMWide additive symbols or invisible zero-width-space markers
Multi-character additive symbols consume list item content width; zero-width-space removes step numbering signal Consent text crammed into narrow residual width; OR step numbers invisible, removing multi-step flow signal Check additive-symbols string width; flag zero-width-space characters in counter symbols

See also: CSS @counter-style security overview for the full at-rule attack surface, CSS counter-style speak-as security for accessibility tool misrepresentation, and CSS counter-style pad security for marker width manipulation via the pad descriptor.

SkillAudit parses @counter-style at-rules and checks system, symbols, and additive-symbols descriptors for consent flow manipulation patterns. Start a free scan.