Security reference · CSS injection · caret-color · Consent manipulation

MCP server CSS caret-color consent security — invisible cursor decoy input consent attack

CSS caret-color controls the color of the text insertion cursor (blinking caret) in input fields and contenteditable elements. It is a low-profile property — many developers are unaware it exists independently of color. Setting caret-color: transparent removes the visible cursor completely while input functionality is fully preserved: the field still accepts keystrokes, fires input events, and updates its value. Attackers exploit this to create decoy input attacks where the visible cursor misleads users about which field is receiving their keystrokes, and to build consent-acceptance mechanisms that activate silently as users type in seemingly unrelated form fields.

caret-color attack surface overview

Attack ID Technique Key mechanism Audit blind spot
SA-CSS-CAR-001 caret-color: transparent + keystroke duplication to consent field Visible username field has normal cursor; transparent-caret consent field receives duplicated keystrokes; user types a username phrase that matches the consent acknowledgment string Consent field is off-screen or zero-size — DOM presence checks pass; input is receiving events but not visible
SA-CSS-CAR-002 caret-color matching field background color Consent input field uses caret-color: #f5f5f5 matching the white input background — cursor is invisible without transparent keyword; user cannot see where they are typing Auditor checks caret-color !== 'transparent' — value is a hex color, not the keyword transparent, audit passes
SA-CSS-CAR-003 Consent checkbox with caret-color: transparent + focus misdirection Tab order places focus on consent checkbox after username; transparent caret on checkbox means no visible focus indicator change; user tabs through the form not realizing focus reached the consent checkbox Auditor checks focus visibility via outline — outline may be present but the caret-color attack applies to the contextual focus state visibility of the checkbox cursor
SA-CSS-CAR-004 caret-color via inherited color override By default caret-color inherits from color — setting color: transparent on an ancestor makes the cursor transparent across all descendant input fields, including the consent field, without setting caret-color directly Auditor checks caret-color property on consent input — value is 'auto' (not transparent); cursor is still invisible because color: transparent ancestor makes caret-color: auto resolve to transparent

Low-profile property: Unlike visibility: hidden or opacity: 0, which are widely known consent-bypass properties, caret-color is a focused property with a very narrow effect — only the blinking text cursor. Auditors scanning for "invisible content" properties typically check visibility, opacity, display, color, and background-color. caret-color is rarely in the list. The property also has no effect on static rendering — screenshots, PDF exports, and visual regression tests will never show the cursor, so the transparency of a cursor is invisible to these tools by design.

Background: how CSS caret-color works

The caret-color CSS property was introduced to allow designers to style the text insertion cursor independently of the text color. Previously, the cursor color was always derived from the color property of the input. With caret-color, you can have dark text in an input but a red cursor, or white text with an auto-colored cursor. The default value is auto, which means the browser chooses a color that contrasts with the current color value. The property applies to any element that has an editable text cursor: <input>, <textarea>, and contenteditable elements. It is an inherited property — setting caret-color: transparent on a form container applies to all descendant inputs.

Attack 1: transparent caret + keystroke duplication to consent field (SA-CSS-CAR-001)

An install form has two input fields: a visible "Username" field and a hidden "consent-acknowledgment" field. The username field has a normal visible cursor. The consent-acknowledgment field has caret-color: transparent, opacity: 0 (or position: absolute; left: -9999px), and is not visible to the user. JavaScript listens for input events on the username field and duplicates every keystroke to the consent field. The consent-acceptance logic watches the consent field for a specific value — for example, if the user types anything (the form accepts any non-empty consent value), or if the user types a phrase that matches a pre-determined consent-acknowledgment string ("I agree", "yes", etc.). The user types their username, not knowing they are simultaneously filling in the consent field.

/* Attack: transparent-caret consent field receives duplicated keystrokes */

/* HTML structure:
   <form id="install-form">
     <input id="username" type="text" placeholder="Username">
     <input id="consent-field" type="text" aria-hidden="true">
   </form>
*/

/* CSS */
#consent-field {
  caret-color: transparent;   /* cursor invisible when field is accidentally focused */
  position: absolute;
  left: -9999px;              /* off-screen */
  opacity: 0;                 /* invisible */
  width: 1px;
  height: 1px;
  /* The field is not visible, but it exists in the DOM and accepts input */
}

/* JavaScript — keystroke duplication */
const usernameField = document.getElementById('username');
const consentField = document.getElementById('consent-field');

usernameField.addEventListener('input', (e) => {
  // Duplicate every keystroke to the consent field
  consentField.value = usernameField.value;

  // Trigger input event on consent field to activate form validation
  consentField.dispatchEvent(new Event('input', { bubbles: true }));
});

// Consent acceptance logic: any non-empty value = accepted
consentField.addEventListener('input', () => {
  if (consentField.value.trim().length > 0) {
    // User has "agreed" by typing their username
    document.querySelector('.install-button').removeAttribute('disabled');
    document.querySelector('[data-consent-state]').setAttribute('data-consent-state', 'accepted');
  }
});

/* Why this uses caret-color: transparent:
   If the user accidentally Tab-navigates to the consent field or if focus
   shifts to it momentarily, a visible cursor blinking in an "invisible" field
   would reveal the field's existence. caret-color:transparent ensures
   the field produces no visual feedback even when focused.

   Detection approach:
   - Check for hidden/off-screen input fields in the same form as the install button
   - Check for JS event listeners on visible fields that write to other fields
   - Check for caret-color:transparent on fields not visually hidden for UX reasons */

/* Variant: consent field is zero-size */
#consent-field-v2 {
  caret-color: transparent;
  width: 0;
  height: 0;
  border: none;
  padding: 0;
  /* Functional: receives input, fires events, holds value */
  /* Visual: zero-size, no border, invisible */
}

SA-CSS-CAR-001 (High). The consent field must exist in the DOM and accept input — both of which are true. The attack is entirely behavioral, exploiting JS event propagation rather than CSS invisibility. caret-color: transparent is a secondary hardening layer (to prevent cursor leakage on accidental focus) rather than the primary attack mechanism. SkillAudit detects this by: (1) finding hidden input fields in install forms; (2) scanning inline and external scripts for addEventListener('input' patterns that write to multiple fields; and (3) checking for caret-color: transparent on any input field that is not visually hidden for documented accessibility or design reasons.

/* Detection: off-screen/invisible input fields with caret-color:transparent */
function detectTransparentCaretConsentField() {
  const findings = [];

  document.querySelectorAll('input, textarea, [contenteditable]').forEach(el => {
    const cs = getComputedStyle(el);
    const caretColor = cs.caretColor;

    // Check for transparent caret
    const isTransparentCaret =
      caretColor === 'transparent' ||
      caretColor === 'rgba(0, 0, 0, 0)' ||
      caretColor === 'rgb(0, 0, 0, 0)';

    if (!isTransparentCaret) return;

    // Check if the field is visually hidden (making caret transparency suspicious)
    const rect = el.getBoundingClientRect();
    const isOffScreen = rect.left < -100 || rect.top < -100 ||
                        rect.right < 0 || rect.bottom < 0;
    const isZeroSize = rect.width === 0 || rect.height === 0;
    const isInvisible = cs.opacity === '0' || cs.visibility === 'hidden';

    if (isOffScreen || isZeroSize || isInvisible) {
      // Hidden field with transparent caret — check if it's in an install form
      const form = el.closest('form');
      if (form) {
        const installButtons = form.querySelectorAll('button, [type="submit"], [data-install]');
        if (installButtons.length > 0) {
          findings.push({
            vuln: 'SA-CSS-CAR-001',
            severity: 'HIGH',
            element: el,
            detail: `input field with caret-color:transparent is hidden (off-screen:${isOffScreen}, ` +
                    `zero-size:${isZeroSize}, invisible:${isInvisible}) in an install form; ` +
                    `may be a keystroke-duplication consent target`
          });
        }
      }
    }
  });

  return findings;
}

// Check for event listeners that copy values between fields (inline script scan)
function scanForKeystrokeDuplication() {
  const scripts = document.querySelectorAll('script:not([src])');
  const patterns = [
    /addEventListener\(['"]input['"]/,
    /\.value\s*=\s*\w+\.value/,
    /dispatchEvent.*input/
  ];

  const findings = [];
  scripts.forEach(script => {
    const text = script.textContent;
    if (patterns.every(p => p.test(text))) {
      findings.push({
        vuln: 'SA-CSS-CAR-001-js',
        severity: 'HIGH',
        element: script,
        detail: `Inline script contains input event listener that copies field values and dispatches synthetic input events — potential keystroke-duplication pattern for consent field targeting`
      });
    }
  });
  return findings;
}

Attack 2: caret-color matching field background color (SA-CSS-CAR-002)

Many auditors check for caret-color === 'transparent' by exact keyword match. CSS color values can be specified as hex codes, RGB, HSL, or named colors. A caret-color: #f9f9f9 on an input with a background-color: #f9f9f9 background makes the cursor invisible — the cursor color matches the background and the blinking cursor is not visible — but the computed caret-color value is not the keyword transparent. It is the hex value #f9f9f9 (or its resolved rgb(249, 249, 249)). An auditor checking caretColor !== 'transparent' finds a non-transparent value and passes the check.

/* Attack: caret-color matches field background — invisible without 'transparent' keyword */
.consent-input {
  background-color: #f5f5f5;
  caret-color: #f5f5f5;        /* matches background exactly */
  color: #1a1a1a;              /* readable text color */
  /* The cursor blinks at #f5f5f5 on a #f5f5f5 background → invisible.
     User cannot see where in the consent field they are typing.
     Combined with user-select:none on consent text labels, the user
     cannot select or copy the consent text, and cannot see cursor position.

     getComputedStyle(el).caretColor → 'rgb(245, 245, 245)'
     Auditor: caretColor !== 'transparent' → PASS (not the transparent keyword)
     But 'rgb(245, 245, 245)' on background-color 'rgb(245, 245, 245)' = invisible cursor */
}

/* Variant: caret-color as CSS variable matching a theme variable */
:root {
  --bg-input: #ffffff;
  --caret-color: var(--bg-input); /* resolves to #ffffff */
}

.consent-input-v2 {
  background-color: var(--bg-input);  /* white */
  caret-color: var(--caret-color);    /* also white */
  /* Cursor is invisible on white input.
     Static analysis of the caret-color property finds 'var(--caret-color)' —
     not 'transparent'. Must trace the custom property to its value. */
}

/* Detection: compare resolved caret-color against resolved background-color */
function detectCaretColorMatchesBackground() {
  const findings = [];

  document.querySelectorAll('input, textarea').forEach(el => {
    const cs = getComputedStyle(el);
    const caretColor = cs.caretColor;
    const backgroundColor = cs.backgroundColor;

    // Normalize to RGB tuple for comparison
    const parseRGB = (color) => {
      const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
      if (!m) return null;
      return [parseInt(m[1]), parseInt(m[2]), parseInt(m[3])];
    };

    const caretRGB = parseRGB(caretColor);
    const bgRGB = parseRGB(backgroundColor);

    if (!caretRGB || !bgRGB) return;

    // Compute WCAG luminance contrast between caret color and background
    const luminance = (r, g, b) => {
      const [rs, gs, bs] = [r, g, b].map(c => {
        const s = c / 255;
        return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
      });
      return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
    };

    const L1 = luminance(...caretRGB);
    const L2 = luminance(...bgRGB);
    const ratio = (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);

    if (ratio < 1.2) { // near-zero contrast — cursor invisible
      // Check if this field is related to consent
      const form = el.closest('form');
      const label = document.querySelector(`label[for="${el.id}"]`);
      const labelText = label ? label.textContent.toLowerCase() : '';
      const CONSENT_KEYWORDS = ['agree', 'authorize', 'accept', 'consent', 'terms'];
      const isConsentRelated = CONSENT_KEYWORDS.some(k => labelText.includes(k)) ||
                               (form && CONSENT_KEYWORDS.some(k =>
                                 form.textContent.toLowerCase().includes(k)));

      if (isConsentRelated) {
        findings.push({
          vuln: 'SA-CSS-CAR-002',
          severity: 'MEDIUM',
          element: el,
          detail: `caret-color (${caretColor}) has contrast ratio ${ratio.toFixed(2)} against ` +
                  `background-color (${backgroundColor}) — cursor is effectively invisible; ` +
                  `field is related to consent acceptance; ` +
                  `computed caretColor is not 'transparent' so keyword checks pass`
        });
      }
    }
  });

  return findings;
}

Attack 3: consent checkbox focus misdirection via invisible caret (SA-CSS-CAR-003)

Form Tab navigation moves focus through elements in DOM order. An install form with fields in order: [Username] → [Email] → [ConsentCheckbox] → [InstallButton] means Tab from Email reaches the ConsentCheckbox. The consent checkbox has caret-color: transparent and no custom outline style visible to the user — the browser's default focus outline may be present but the consent checkbox's cursor state shows no additional indicator. A user tab-navigating through the form may not realize focus has reached the consent checkbox. If the user then presses Space (the standard keyboard activation for checkboxes), they activate the checkbox without noticing they did so. The transparent caret on the checkbox removes the secondary visual cue that focus is on an input element.

/* Attack: consent checkbox with transparent caret + suppressed focus outline */
.consent-checkbox {
  caret-color: transparent;     /* no cursor indicator when focused */
  outline: none;                 /* suppress default focus ring */
  /* Combined: the checkbox has no visible focus indicator.
     If the user tabs to it expecting to be on the email field still,
     they press Space to "confirm" something — activating the checkbox. */
}

/* Detection */
function detectConsentCheckboxFocusEvasion() {
  const findings = [];

  document.querySelectorAll('input[type="checkbox"]').forEach(el => {
    const cs = getComputedStyle(el);
    const label = document.querySelector(`label[for="${el.id}"]`);
    const labelText = label ? label.textContent.toLowerCase() : '';
    const CONSENT_KEYWORDS = ['agree', 'authorize', 'accept', 'consent', 'terms', 'install'];
    if (!CONSENT_KEYWORDS.some(k => labelText.includes(k))) return;

    const caretColor = cs.caretColor;
    const outline = cs.outlineStyle;
    const outlineWidth = parseFloat(cs.outlineWidth) || 0;

    const isTransparentCaret = caretColor === 'transparent' ||
                               caretColor === 'rgba(0, 0, 0, 0)';
    const isFocusInvisible = outline === 'none' || outlineWidth === 0;

    if (isTransparentCaret && isFocusInvisible) {
      findings.push({
        vuln: 'SA-CSS-CAR-003',
        severity: 'MEDIUM',
        element: el,
        detail: `consent checkbox has caret-color:${caretColor} and outline:${outline}/width:${outlineWidth}px; ` +
                `no visible focus indicator when tab-navigating — user may not realize focus reached consent checkbox`
      });
    }
  });

  return findings;
}

Attack 4: inherited color: transparent makes caret-color: auto resolve to transparent (SA-CSS-CAR-004)

The default caret-color: auto means the browser picks a cursor color based on the element's color property value. If an ancestor element has color: transparent (or a very low-opacity color), and that color value inherits to the consent input field, the caret-color: auto on the input resolves to a transparent or near-invisible color. An auditor checking caret-color on the consent input finds the value 'auto' — not transparent — and passes the check. But the effective cursor color, derived from the inherited transparent color, is transparent. The cursor is invisible. This is especially problematic because color: transparent on a wrapper div is used in other CSS attacks (like background-clip: text whitewash), so the same rule responsible for a text-color attack may also make the cursor invisible as a side effect.

/* Attack: color:transparent ancestor makes caret-color:auto resolve transparent */

/* Ancestor with transparent color (e.g., from a background-clip:text attack) */
.consent-wrapper {
  color: transparent;
  background-image: linear-gradient(white, white); /* background-clip:text whitewash */
  -webkit-background-clip: text;
  background-clip: text;
}

/* Consent input inherits color: transparent */
.consent-input {
  /* No caret-color specified — defaults to caret-color: auto */
  /* caret-color: auto resolves based on inherited 'color' value */
  /* Inherited color: transparent → caret-color auto → transparent cursor */

  /* Auditor check:
     getComputedStyle(input).caretColor → 'auto' (not transparent)
     getComputedStyle(input).color → 'rgba(0, 0, 0, 0)' (transparent — from inheritance)
     Auditor checking caretColor: finds 'auto' — PASSES
     Auditor NOT checking whether auto resolves to transparent via color inheritance: misses it */
}

/* Detection: check if caret-color:auto would resolve to invisible via color inheritance */
function detectAutoCaretTransparentInheritance() {
  const findings = [];

  document.querySelectorAll('input, textarea').forEach(el => {
    const cs = getComputedStyle(el);
    const caretColor = cs.caretColor;

    // Only flag caret-color:auto — resolved values (not 'auto') are checked by SA-CSS-CAR-002
    if (caretColor !== 'auto') return;

    // Check the inherited color value
    const color = cs.color;
    const parseAlpha = (c) => {
      const m = c.match(/rgba?\(\d+,\s*\d+,\s*\d+,?\s*([\d.]+)?\)/);
      return m ? (m[1] !== undefined ? parseFloat(m[1]) : 1) : 1;
    };

    const alpha = parseAlpha(color);
    if (alpha < 0.15) {
      // Inherited color is near-transparent → caret-color:auto resolves near-transparent
      const form = el.closest('form');
      const CONSENT_KEYWORDS = ['agree', 'authorize', 'accept', 'consent'];
      const formText = form ? form.textContent.toLowerCase() : '';
      if (CONSENT_KEYWORDS.some(k => formText.includes(k))) {
        findings.push({
          vuln: 'SA-CSS-CAR-004',
          severity: 'MEDIUM',
          element: el,
          detail: `caret-color:auto on consent input; inherited color is near-transparent (alpha:${alpha.toFixed(2)}); ` +
                  `caret-color:auto resolves to transparent via color inheritance; ` +
                  `auditors checking getComputedStyle().caretColor find 'auto' — pass check incorrectly`
        });
      }
    }
  });

  return findings;
}

SkillAudit detection

HIGH SA-CSS-CAR-001: hidden input fields with caret-color: transparent in install forms, combined with JS event listeners that duplicate keystrokes between fields — transparent caret prevents accidental cursor visibility that would reveal the hidden consent field. SkillAudit scans inline scripts for keystroke-duplication patterns alongside invisible field detection.
MEDIUM SA-CSS-CAR-002: caret-color set to a hex or RGB value that matches the field's background color — cursor is invisible but caretColor !== 'transparent' passes keyword-based checks. SkillAudit computes the WCAG contrast ratio between the resolved caret-color and background-color values on consent-related input fields.
MEDIUM SA-CSS-CAR-003: consent checkbox with caret-color: transparent and no outline — no visual focus indicator when the checkbox receives Tab focus; user may activate the checkbox unknowingly via Space key. SkillAudit checks focus indicator visibility (outline + caret-color) on consent-related checkboxes.
MEDIUM SA-CSS-CAR-004: caret-color: auto (default) on a consent input whose inherited color value is near-transparent — the auto cursor resolves to a transparent color via color inheritance without a direct caret-color: transparent declaration. SkillAudit checks the inherited color alpha channel on inputs with caret-color: auto.

Run SkillAudit to detect SA-CSS-CAR patterns in any MCP server before install. SkillAudit scans for transparent and background-matching caret colors on consent-related input fields, traces color inheritance to detect implicit transparent cursor states, and detects keystroke-duplication patterns in inline scripts that use off-screen inputs as consent-activation targets.