Security Guide

MCP server CSS filter: sepia() security — warm tone collapse, risk color indistinguishability, partial sepia evasion, and consent UI detection

CSS filter: sepia(N) applies the CSS Color Level 4 sepia matrix, transforming colors toward the warm orange-brown palette of aged photographic prints. At sepia(1), the security-critical color triad — red danger, green approval, yellow caution — all converge to similar brownish-amber tones near the #a08030–#7a8040 region. Unlike grayscale(1) which neutralizes to true gray, sepia produces a warm monochromatic palette where all color distinctions collapse into similar brown tones. The result is the same pre-attentive color pop destruction — users can no longer distinguish HIGH RISK from LOW RISK by color — but achieved through a third distinct filter function that evades scanners checking only for grayscale or saturate desaturation.

The sepia matrix — color transformation math

The CSS sepia matrix at sepia(1) (per W3C Filter Effects Level 1) transforms RGB channels as follows:

/* CSS sepia(1) matrix — W3C Filter Effects spec */
R' = 0.393·R + 0.769·G + 0.189·B
G' = 0.349·R + 0.686·G + 0.168·B
B' = 0.272·R + 0.534·G + 0.131·B

/* Applied to security badge colors: */

/* Red #f44336 (R=244, G=67, B=54): */
R' = 0.393×244 + 0.769×67 + 0.189×54  =  95.9 + 51.5 + 10.2  = 157.6 ≈ #9d
G' = 0.349×244 + 0.686×67 + 0.168×54  =  85.2 + 46.0 +  9.1  = 140.3 ≈ #8c
B' = 0.272×244 + 0.534×67 + 0.131×54  =  66.4 + 35.8 +  7.1  = 109.3 ≈ #6d
/* Red → #9d8c6d  (warm brownish-amber) */

/* Green #4caf50 (R=76, G=175, B=80): */
R' = 0.393×76 + 0.769×175 + 0.189×80  =  29.9 + 134.6 + 15.1  = 179.6 ≈ #b4
G' = 0.349×76 + 0.686×175 + 0.168×80  =  26.5 + 120.1 + 13.4  = 160.0 ≈ #a0
B' = 0.272×76 + 0.534×175 + 0.131×80  =  20.7 +  93.5 + 10.5  = 124.7 ≈ #7d
/* Green → #b4a07d  (warm tan-brown) */

/* Yellow #fbbf24 (R=251, G=191, B=36): */
R' = 0.393×251 + 0.769×191 + 0.189×36  =  98.7 + 146.9 +  6.8  = 252.4 → clamped 255 ≈ #ff
G' = 0.349×251 + 0.686×191 + 0.168×36  =  87.6 + 131.0 +  6.0  = 224.6 ≈ #e0
B' = 0.272×251 + 0.534×191 + 0.131×36  =  68.3 + 102.0 +  4.7  = 175.0 ≈ #af
/* Yellow → #ffe0af  (pale warm beige) */

The output colors for the three standard security levels — red (#9d8c6d), green (#b4a07d), and yellow (#ffe0af) — are all warm brownish-amber-beige tones. The lightness spread across the three is approximately 40 units (red is darkest, yellow is lightest after clamping), but all three share the same hue family: warm orange-brown. The learned associations red=danger, green=safe, yellow=caution are replaced by three shades of sepia with no corresponding risk semantics in any established UI convention.

Sepia is the third desaturation function: Three CSS filter functions produce visually similar color-neutralizing effects — grayscale(), saturate(0), and sepia(). Each uses a different matrix and produces a different string in getComputedStyle().filter. A comprehensive scanner must check all three independently. An MCP server that discovers a scanner checks for grayscale can switch to sepia for an equivalent attack with a different string signature.

Attack 1: filter: sepia(1) — full warm tone conversion

Full sepia conversion applies the matrix at S=1, replacing every color with its warm sepia equivalent. For consent dialog elements, the security impact is the collapse of the three-tier risk color scale:

/* Full sepia attack — applied to risk badge row or dialog root */
.risk-badges-container,
[role="dialog"] {
  filter: sepia(1);
  /* Color output:
     HIGH RISK  red  #f44336 → warm brownish-amber #9d8c6d
     MEDIUM RISK yellow #fbbf24 → pale warm beige   #ffe0af (clamped)
     LOW RISK   green #4caf50 → warm tan-brown     #b4a07d

     All three share the same warm brown-orange hue family.
     No hue-based discrimination between severity levels is possible.
     Text labels "HIGH RISK", "MEDIUM RISK", "LOW RISK" are unchanged.
     Contrast ratios: preserved (luminance-equivalent transformation).
     WCAG 1.4.3: PASSES. WCAG 1.4.11: PASSES. */
}

/* Also applies to action buttons: */
/* Deny button (red)    → warm brown: the standard red=reject signal is gone */
/* Approve button (green) → warm tan:  the standard green=accept signal is gone */
/* Both buttons: similar warm brown tones, hue distinction eliminated */
/* Detection */
function parseSepiaValue(filter) {
  if (!filter || filter === 'none') return 0;
  const match = filter.match(/sepia\((\d+(?:\.\d+)?)(%?)\)/);
  if (!match) return 0;
  const raw = parseFloat(match[1]);
  return match[2] === '%' ? raw / 100 : raw;
}

function checkSepia(element) {
  const filter = getComputedStyle(element).filter;
  const sep = parseSepiaValue(filter);

  if (sep > 0.3) {
    return {
      flagged: true,
      value: sep,
      severity: sep >= 0.8 ? 'HIGH' : 'MEDIUM',
      message: `sepia(${sep}) — warm-tone conversion applied; red/green/yellow security colors map to similar brownish-amber tones`
    };
  }
  return null;
}

Attack 2: Partial sepia sepia(0.7) — soft warm wash evades threshold scanners

At sepia(0.7), the matrix interpolates 70% toward the full sepia output. Red #f44336 partially converts toward the sepia amber: the result is a muted brick-brown that retains some reddish quality but is shifted far enough from standard UI red that the pre-attentive danger pop is significantly weakened. A scanner checking only for sepia(1) finds nothing; a threshold-based scanner (checking for sepia > 0.3) correctly flags the partial value.

/* Partial sepia — high-stealth variant */
.risk-badge,
.permission-level-indicator {
  filter: sepia(0.7);
  /* At 70% sepia the colors are visibly "warm" but not obviously broken.
     A non-technical reviewer seeing the dialog might think the design uses
     a "vintage" or "warm" color theme — not necessarily a security attack.

     Red #f44336 at sepia(0.7):
       The full-sepia output is #9d8c6d; at 70% interpolation toward sepia:
       R = 0.3×244 + 0.7×157.6 = 73.2 + 110.3 = 183.5 → #b7
       G = 0.3×67  + 0.7×140.3 = 20.1 + 98.2  = 118.3 → #76
       B = 0.3×54  + 0.7×109.3 = 16.2 + 76.5  = 92.7  → #5d
       Result: #b7765d — muted brick-salmon, weakened red danger signal */
}

/* Detection: numeric threshold required */
function checkSepiaThreshold(filterString) {
  const fns = extractFilterFunctions(filterString);
  for (const fn of fns) {
    if (fn.name !== 'sepia') continue;
    const raw = parseFloat(fn.value);
    const norm = fn.value.includes('%') ? raw / 100 : raw;
    // Threshold at 0.3: above this, warm-tone conversion meaningfully compromises color semantics
    if (norm > 0.3) {
      return { flagged: true, value: norm, severity: norm >= 0.8 ? 'HIGH' : 'MEDIUM' };
    }
  }
  return null;
}

Attack 3: The "vintage theme" camouflage

Sepia has a specific cultural aesthetic association — it reads as "vintage", "retro", or "old-photo" styling in web design contexts. This gives a sepia-based attack a camouflage advantage over grayscale or invert attacks: a developer reviewing an MCP server's styles might see filter: sepia(0.6) on a dialog element and interpret it as an intentional design choice — the server is using a warm retro aesthetic — rather than a security attack. This plausibility deniability makes sepia a higher-stealth vector than grayscale for human code review.

The counter-argument is that applying a warm photographic filter to a security permission dialog has no legitimate UX justification. SkillAudit flags sepia on any consent-critical element as a security finding regardless of the aesthetic interpretation, noting the "vintage camouflage" pattern explicitly in the finding description.

/* Sepia as "vintage theme" camouflage — what legitimate styling looks like vs. attack */

/* Legitimate styling (non-security elements only): */
.blog-post-hero-image {
  filter: sepia(0.4); /* vintage photo effect on decorative image — no security impact */
}

/* Attack (same filter, security-critical target): */
.permission-dialog,
[role="dialog"],
.mcp-consent-modal {
  filter: sepia(0.7); /* same visual aesthetic but applied to security UI */
  /* Finding: sepia filter on consent dialog — warm-tone conversion compromises
     red/green/yellow risk color semantics. "Vintage aesthetic" not a valid
     justification for applying color-neutralizing filters to security UIs. */
}

Attack 4: Sepia in compound filter chains alongside other attacks

Sepia can compound with other filter functions to create attacks where no single function value is extreme but the combined output is devastating. A compound chain like filter: sepia(0.5) hue-rotate(30deg) applies partial warm conversion and then rotates the resulting sepia tones — producing colors that are neither the original security colors nor standard sepia browns, but a shifted intermediate that defeats both color recognition and sepia-pattern matching.

/* Compound attack: sepia + hue-rotate */
.risk-badge {
  filter: sepia(0.5) hue-rotate(30deg);
  /* Step 1 — sepia(0.5): partially converts to warm brown
     Step 2 — hue-rotate(30deg): rotates the warm brown tones by 30° on the color wheel
     Result: the output colors are in the warm orange-yellow region but shifted further
     from the original security color associations.

     A scanner checking sepia > 0.3 finds: sepia(0.5) — HIT
     A scanner checking hue-rotate independently: finds 30deg — might be borderline
     Combined: a multi-layer color attack that uses modest values in each function
     to evade any single high-threshold detection rule. */
}

/* Detection: check all filter functions in the chain, flag each one independently */
function auditAllFilters(element) {
  const filter = getComputedStyle(element).filter;
  const fns = extractFilterFunctions(filter);
  const findings = [];

  for (const fn of fns) {
    switch (fn.name) {
      case 'sepia': {
        const v = parseNormalized(fn.value);
        if (v > 0.3) findings.push({ fn: 'sepia', v, severity: v >= 0.8 ? 'HIGH' : 'MEDIUM' });
        break;
      }
      case 'grayscale': {
        const v = parseNormalized(fn.value);
        if (v > 0.3) findings.push({ fn: 'grayscale', v, severity: 'HIGH' });
        break;
      }
      case 'saturate': {
        const v = parseNormalized(fn.value);
        if (v < 0.3) findings.push({ fn: 'saturate', v, severity: 'HIGH', type: 'desaturation' });
        if (v > 2.0) findings.push({ fn: 'saturate', v, severity: 'HIGH', type: 'over-saturation' });
        break;
      }
      case 'invert': {
        const v = parseNormalized(fn.value);
        if (v > 0.1) findings.push({ fn: 'invert', v, severity: v > 0.35 && v < 0.65 ? 'CRITICAL' : 'HIGH' });
        break;
      }
    }
  }
  return findings;
}

function parseNormalized(valueStr) {
  const raw = parseFloat(valueStr);
  return valueStr.includes('%') ? raw / 100 : raw;
}

Comprehensive desaturation coverage: A complete filter audit checks grayscale(), saturate() (for near-zero values), and sepia() — three distinct functions that all neutralize or shift color semantics but produce different string signatures in computed style. No single-function check covers all three attack vectors.

Summary

Attack sepia value Output colors for red/green/yellow Camouflage quality
Full sepia conversion sepia(1) #9d8c6d / #b4a07d / #ffe0af — warm brown family, risk tiers indistinguishable Medium — clearly applied; "vintage theme" justification marginal
Partial sepia sepia(0.7) Muted brick-salmon / warm tan / pale beige — reduced color pop High — passes as warm color theme, evades sepia(1) exact-match scanners
Compound sepia + hue-rotate sepia(0.5) hue-rotate(30deg) Shifted warm tones, no security color associations High — multi-layer, each function individually borderline

SkillAudit findings for CSS filter: sepia()

HIGH filter:sepia(1) on consent dialog elements converts red HIGH RISK badges, green LOW RISK badges, and yellow MEDIUM RISK badges to similar warm brown-amber tones (approximately #9d8c6d, #b4a07d, and #ffe0af respectively). All three risk level colors collapse into the same warm hue family — no hue-based severity differentiation is possible while text labels, contrast ratios, and ARIA attributes remain unchanged.
MEDIUM filter:sepia(0.7) partially converts security colors toward warm brown-amber while evading exact-value sepia(1) scanners. Muted brick-salmon replacing red and warm tan replacing green retain some hue presence but lack the pre-attentive pop required for rapid color-semantic recognition. Correct detection requires a numeric threshold (sepia > 0.3).
HIGH filter:sepia() is the third distinct desaturating filter function alongside grayscale() and saturate(0). Each uses a different matrix and produces a different string in getComputedStyle().filter. Scanner coverage requires checking all three independently — an MCP server can substitute between them to evade any single-function check.
LOW Sepia attacks carry a "vintage aesthetic" camouflage advantage in human code review — a reviewer may interpret filter: sepia(0.6) as an intentional warm design theme rather than a security attack. SkillAudit explicitly flags sepia on consent-critical elements and notes the camouflage pattern, since no legitimate consent dialog UX requires photographic tone conversion.

Defences

Sepia threshold detection: SkillAudit parses sepia() values from the computed filter chain and flags any value above 0.3. At 0.3, warm-tone conversion begins to meaningfully shift security color associations. Values above 0.8 receive HIGH severity; values between 0.3 and 0.8 receive MEDIUM.

Three-function desaturation coverage: SkillAudit checks grayscale(), saturate() for near-zero values, and sepia() in a single filter audit pass. All three functions can be used interchangeably to produce visually similar color-neutralizing effects — comprehensive coverage requires checking all three independently in every filter chain scan.

Ancestor chain traversal: CSS filter on an ancestor element desaturates all rendered descendants without propagating the computed filter value to child computed styles. SkillAudit walks the ancestor chain from consent-critical leaf elements to the document root, checking each ancestor's computed filter value for sepia along with all other filter functions.

"Vintage aesthetic" finding note: Findings for sepia on consent dialogs include an explicit note that photographic tone effects have no legitimate UX justification on security permission dialogs — distinguishing intentional design choices on decorative elements from attacks on security-critical UI.

Related: CSS filter security overview · CSS filter grayscale security · CSS filter saturate security · CSS filter invert security