Security Guide

MCP server CSS @counter-style negative consent security — countdown timer disruption and negative-counter display attacks

CSS @counter-style negative specifies the prefix applied to negative counter values (default: a hyphen-minus -). An MCP server replacing the negative prefix with an empty string makes consent countdown timers appear to count forward ("5, 4, 3, 2, 1") rather than down ("-5, -4, -3, -2, -1"). Wide-character substitutions push negative-numbered consent list items off-screen. Unicode minus lookalikes break positional parsing in RTL-aware contexts. The DOM counter value is unchanged; only the rendered prefix is affected.

How @counter-style negative works

The negative descriptor accepts one or two strings: a prefix that is prepended to the marker text when the counter value is negative, and an optional suffix appended after the marker text. The default value is negative: '-' (ASCII hyphen-minus, U+002D). Changing this descriptor does not change the underlying CSS counter value — only its visual representation. A countdown list element with counter(-5) continues to store -5; only the rendering changes. This means DOM APIs that return the counter value still return -5, while the rendered text shows "5" (if negative is set to empty string).

/* Default behavior */
@counter-style countdown {
  system: integer;
  negative: '-'; /* prepended to negative values */
  /* counter(-5) renders as: "-5" */
  /* counter(-1) renders as: "-1" */
}

/* Attack: empty string removes the negative sign */
@counter-style countdown {
  system: integer;
  negative: ''; /* empty string — no prefix for negative values */
  /* counter(-5) renders as: "5" (looks positive) */
  /* counter(-1) renders as: "1" (looks positive) */
  /* Countdown list: looks like 5, 4, 3, 2, 1 (ascending/forward) */
  /* Actual values: -5, -4, -3, -2, -1 (descending, expired) */
}

/* Consent timer context:
   A "please read for 5 seconds" timer uses a CSS counter starting at -5
   and incrementing to 0. The display is meant to show -5, -4, -3, -2, -1, 0.
   With negative:'', display shows: 5, 4, 3, 2, 1, 0.
   User sees counting down from 5 to 0 — appears to count a new 5-second period.
   But the timer is at negative values — already expired or at epoch.
*/

Context dependency: Negative counter values appear in step-back navigation UI ("Go back 3 steps"), countdown timers, debt/balance displays in financial disclosures, and reverse-indexed lists. The attack surface depends on how the consent UI uses counters. Countdown timers are the highest-value target.

Attack 1 (HIGH): empty negative string — countdown timer direction reversal

The attack replaces the negative prefix with an empty string on the counter style used by a consent countdown timer. The timer visually appears to count down normally (5, 4, 3, 2, 1) but is actually displaying negative values — the timer may have already elapsed, or the countdown period may be calculated from a different baseline. The user waits for what appears to be a fresh 5-second countdown but may be interacting with expired consent state.

/* Attack 1: empty negative prefix on countdown counter */
@counter-style consent-timer {
  system: integer;
  negative: ''; /* removes minus sign */
}

/* How the timer works (without attack):
   CSS counter "consent-countdown" decrements from 0 to -10
   @keyframes countdown { to { counter-increment: consent-countdown -1 } }
   Display: 0, -1, -2, ... -10 (shows clearly negative = timer running)
   Accept button enabled when counter reaches -10 (animation complete)
*/

/* With attack: counter still goes from 0 to -10
   Display: 0, 1, 2, ... 10 (APPEARS to count UP from 0)
   User perceives: "counting up to some threshold"
   Actual state: negative values → timer elapsed → Accept was always enabled?
   Or: UI uses displayed value to gate Accept button via JavaScript:
   if (parseInt(displayedMarker) >= 10) { enableAccept(); }
   Attack makes timer reach 10 immediately from display perspective
*/

/* Conditional variant: attack only when timer is near zero */
/* (not directly possible in CSS alone — attack applies uniformly to all
   negative values of that counter style) */

Attack 2 (HIGH): wide-character negative prefix — items pushed off-screen

Setting the negative prefix to a very wide Unicode character string creates oversized markers for all negative-counter list items. If the consent list uses negative counter values for any items, those items are pushed off-screen. A typical use case: a multi-step consent flow uses a counter that starts at -3 for the pre-consent steps. Setting negative: '\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0' makes those pre-consent steps' markers 20+ characters wide, displacing their content off-screen.

/* Attack 2: wide negative prefix displaces items off-screen */
@counter-style multi-step-consent {
  system: integer;
  negative: '\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0'; /* 25 NBSP */
  /* Negative counter items (steps -3, -2, -1):
     marker = 25 NBSP + "3" = ~200px wide marker
     Content start position: ~200px from list container edge
     360px mobile container: 200px marker → 160px for content
     40px container (sidebar steps): all content off-screen
  */
}

/* Multi-step consent flow using negative counters for "pre-conditions":
   Step -3: "Verify your email address" (negative counter)
   Step -2: "Read the terms below" (negative counter)
   Step -1: "Confirm your age" (negative counter)
   Step 0:  "Complete" (zero counter — not affected by negative descriptor)

   Attack: steps -3, -2, -1 all have 25-NBSP markers
   Content displaced off-screen on mobile
   User only sees step 0 (Complete)
*/

Attack 3 (MEDIUM): Unicode minus lookalike — RTL context parsing disruption

The default hyphen-minus (U+002D) is directionally neutral. Replacing it with a right-to-left mark or a Unicode minus sign (U+2212, MINUS SIGN) can affect marker rendering in RTL-aware user agents or in consent containers with mixed directionality. The U+2212 MINUS SIGN visually matches the hyphen-minus but is treated differently by text shaping engines and TTS systems. In some contexts, U+2212 triggers different bidirectional text reordering, placing the minus sign after the number rather than before it in RTL text flow.

/* Attack 3: Unicode MINUS SIGN vs hyphen-minus */
@counter-style rtl-attack-counter {
  system: integer;
  negative: '\2212'; /* U+2212 MINUS SIGN — visually identical to '-' */
  /* In RTL text context:
     U+002D (hyphen-minus): directionally neutral → renders before number
     U+2212 (MINUS SIGN): mathematics minus → may be treated as directional
     → In some browsers with RTL consent containers:
        marker renders as "3−" instead of "−3"
        Positional context lost — user cannot determine if value is -3 or "3−" (suffix?)
  */
}

/* TTS disruption:
   TTS announces U+002D as "negative three" or "minus three"
   TTS announces U+2212 as "three minus" or spells "three m-i-n-u-s"
   Screen reader navigation by countdown position broken
*/

/* Compound with speak-as:
@counter-style rtl-attack-counter {
  system: integer;
  negative: '\2212';
  speak-as: spell-out; /* "t-h-r-e-e m-i-n-u-s" — each character spelled */
}

Attack 4 (MEDIUM): negative suffix — suffix displaced after counter value

The negative descriptor can take a second argument: a string appended after the marker text for negative values. An MCP server can use this suffix position to insert Unicode characters that alter the perceived counter value. A suffix of ' ≥' (space + greater-than-or-equal) after a negative counter "-5" produces "-5 ≥", suggesting "at or above -5" rather than "five seconds remaining" — changing the semantic interpretation of the timer in ways that may induce premature acceptance.

/* Attack 4: negative suffix semantic manipulation */
@counter-style consent-countdown {
  system: integer;
  negative: '-' ' sec'; /* prefix: '-', suffix: ' sec' */
  /* counter(-5) → "-5 sec" — appears as "five seconds remaining" */
  /* This looks legitimate — but the suffix can be used adversarially */
}

/* Adversarial suffix variant */
@counter-style consent-countdown-attack {
  system: integer;
  negative: '' ' (complete)'; /* no prefix + ' (complete)' suffix */
  /* counter(-5) → "5 (complete)" — suggests step is already done */
  /* counter(-1) → "1 (complete)" — step 1 complete */
  /* User sees: "1 (complete)", "2 (complete)", "3 (complete)"
     Perceives all negative-counter steps as already completed
     Skips reading and interacting with them
  */
}

/* Detection: check negative descriptor second argument for semantic manipulation strings */

Detection

/* Enumerate @counter-style rules and check negative descriptor */
function auditCounterStyleNegative() {
  const attacks = [];
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        if (rule instanceof CSSCounterStyleRule) {
          const negative = rule.negative; /* e.g. "'-'" or "''" or "'-' ' sec'" */

          if (!negative) continue;

          /* Extract prefix (first quoted string) */
          const parts = negative.match(/'([^']*)'/g) || [];
          const prefix = parts[0] ? parts[0].slice(1, -1) : '';
          const suffix = parts[1] ? parts[1].slice(1, -1) : '';

          /* Flag: empty negative prefix (removes minus sign) */
          if (prefix === '') {
            attacks.push({
              name: rule.name,
              issue: 'empty negative prefix — minus sign suppressed',
              negative
            });
          }

          /* Flag: wide prefix (>10 chars — displacement attack) */
          if (prefix.length > 10) {
            attacks.push({
              name: rule.name,
              issue: `wide negative prefix (${prefix.length} chars) — displacement`,
              negative
            });
          }

          /* Flag: Unicode MINUS SIGN (U+2212) vs hyphen-minus */
          if (prefix.includes('−')) {
            attacks.push({
              name: rule.name,
              issue: 'Unicode MINUS SIGN (U+2212) in negative prefix — RTL disruption',
              negative
            });
          }

          /* Flag: suspicious suffix strings */
          if (/complete|done|accepted|agreed|ok/i.test(suffix)) {
            attacks.push({
              name: rule.name,
              issue: `semantic manipulation in negative suffix: "${suffix}"`,
              negative
            });
          }
        }
      }
    } catch (e) { /* cross-origin */ }
  }
  return attacks;
}
AttackSeverityCounter value in DOM affected?Detection method
Empty negative prefix — countdown direction reversalHIGHNo (CSS rendering only)Check CSSCounterStyleRule.negative for empty string prefix
Wide negative prefix — items off-screenHIGHNoCheck negative prefix character length; flag if > container threshold
Unicode MINUS SIGN (U+2212) — RTL/TTS disruptionMEDIUMNoCheck for U+2212 in negative descriptor; compare to default U+002D
Semantic-manipulation suffix ("complete", "done")MEDIUMNoCheck negative descriptor second argument for completion-implying strings