← Blog

MCP server SVG feColorMatrix sepia consent security

Four SA-CSS-CME consent attack patterns using feColorMatrix type="matrix" with standard sepia coefficients — a warm-tone transform that maps both red danger badges and green safe badges to nearly identical brownish outputs, destroying the traffic-light color-severity signal without making the consent panel invisible, and additionally making yellow warning badges near-invisible on white backgrounds.

By SkillAudit  |  24 Oct 2026

The sepia tone transform is a well-known photographic effect that replaces chromatic colors with warm brownish-orange tones. In the SVG filter pipeline it is implemented as a feColorMatrix type="matrix" with a specific set of cross-channel mixing coefficients derived from warm-tone photographic film emulsion characteristics. The W3C CSS filter specification defines a canonical sepia matrix. While sepia is familiar as an aesthetic filter and has no security connotation in photographic contexts, applied to an MCP consent dialog it executes a badge-color collapse attack: the red-green traffic light severity system that consent UIs rely on to communicate permission danger is destroyed because both red and green inputs map to numerically similar warm-brown outputs.

The sepia attack is distinct from alpha-zeroing (which makes content invisible) and channel-swap (which reverses color meanings). Sepia makes the consent panel fully visible but renders all permission-severity badge colors indistinguishable from each other — a monochromatic brownish visual where the user cannot tell whether a permission is safe, cautionary, or dangerous.

Semantic collapse, not invisibility: The sepia attack makes the consent panel clearly readable but destroys its permission-severity color encoding. Body text (dark gray/black) is only slightly shifted in luminance and remains readable. The primary damage is to colored permission badges. A consent dialog with six permissions — two dangerous (red), two caution (yellow), two safe (green) — appears after sepia as a monochromatic brownish panel where all six badges look nearly the same. The user cannot identify which permissions are dangerous.

Sepia matrix coefficients

/* Standard sepia matrix (W3C CSS filter spec equivalent via feColorMatrix):
   R' = 0.393×R + 0.769×G + 0.189×B + 0×A + 0
   G' = 0.349×R + 0.686×G + 0.168×B + 0×A + 0
   B' = 0.272×R + 0.534×G + 0.131×B + 0×A + 0
   A' = 0×R + 0×G + 0×B + 1×A + 0

   values="0.393 0.769 0.189 0 0
           0.349 0.686 0.168 0 0
           0.272 0.534 0.131 0 0
           0     0     0     1 0"

   Key property: all R', G', B' output rows have the SAME column weight ratios
   (0.393:0.769:0.189 ≈ 0.349:0.686:0.168 ≈ 0.272:0.534:0.131 ≈ 2:4:1)
   This means all input colors map to outputs with fixed channel ratios (warm tone).
   The output hue is always the same warm-brown regardless of input hue.
   Only the luminance value varies — different inputs produce different brightness
   of the same brown tone. */

SA-CSS-CME-001: sepia collapses red and green badge outputs to near-identical brownish tones

The standard sepia matrix maps the red danger badge (#EF4444) and the green safe badge (#22C55E) to warm-brown values that differ by only 0.051 in R, 0.048 in G, and 0.037 in B — visually the same brownish tone. Any consent UI that uses these colors (or similar traffic-light values) to indicate permission severity will have those distinctions destroyed.

/* Red danger badge #EF4444:
   sRGB: R=0.937, G=0.267, B=0.267
   Linear (sRGB→linear via IEC 61966-2-1):
     R_lin = ((0.937+0.055)/1.055)^2.4 ≈ 0.871
     G_lin = ((0.267+0.055)/1.055)^2.4 ≈ 0.057
     B_lin = ((0.267+0.055)/1.055)^2.4 ≈ 0.057

   After sepia:
     R' = 0.393×0.871 + 0.769×0.057 + 0.189×0.057 = 0.342 + 0.044 + 0.011 = 0.397
     G' = 0.349×0.871 + 0.686×0.057 + 0.168×0.057 = 0.304 + 0.039 + 0.010 = 0.353
     B' = 0.272×0.871 + 0.534×0.057 + 0.131×0.057 = 0.237 + 0.030 + 0.007 = 0.274
   Output: warm brown (R≈0.397, G≈0.353, B≈0.274)

   Green safe badge #22C55E:
   sRGB: R=0.133, G=0.773, B=0.369
   Linear:
     R_lin ≈ 0.016, G_lin ≈ 0.557, B_lin ≈ 0.116

   After sepia:
     R' = 0.393×0.016 + 0.769×0.557 + 0.189×0.116 = 0.006 + 0.428 + 0.022 = 0.456
     G' = 0.349×0.016 + 0.686×0.557 + 0.168×0.116 = 0.006 + 0.382 + 0.019 = 0.407
     B' = 0.272×0.016 + 0.534×0.557 + 0.131×0.116 = 0.004 + 0.297 + 0.015 = 0.316
   Output: warm brown (R≈0.456, G≈0.407, B≈0.316)

   Difference between red badge output and green badge output:
     ΔR = 0.456-0.397 = 0.059 (out of 1.0)
     ΔG = 0.407-0.353 = 0.054
     ΔB = 0.316-0.274 = 0.042
   Total Euclidean color distance: sqrt(0.059²+0.054²+0.042²) ≈ 0.086
   → Visually: both are warm brownish shades; the difference is noticeable in a direct
     side-by-side comparison but NOT when viewing a consent panel where badges are
     separated by prose text. In context, both read as "brown" — severity lost. */

The luminance of the red badge output (L = 0.2126×0.397 + 0.7152×0.353 + 0.0722×0.274 = 0.084 + 0.252 + 0.020 = 0.356) and the green badge output (L = 0.2126×0.456 + 0.7152×0.407 + 0.0722×0.316 = 0.097 + 0.291 + 0.023 = 0.411) differ by only 0.055 in luminance. Both badges are clearly visible against a white background (contrast 2.3:1 and 2.1:1 respectively — both fail WCAG AA as badge text, but both are perceivable). The attack is not about invisibility but about undifferentiability.

WCAG compliance is insufficient: The sepia attack outputs may be perceivable (non-zero contrast) but their WCAG failures are not the primary attack vector. Even if the sepia output somehow passed WCAG contrast, the attack succeeds because both red and green badges map to the same brownish hue — the color-severity channel is destroyed. Scanners that only check WCAG contrast and do not verify badge-color semantic preservation will miss this attack entirely.

SA-CSS-CME-002: dark body text through sepia — low luminance impact

Dark consent body text (#333333, all channels equal at sRGB 0.2, linear≈0.033) maps through the sepia matrix as follows: since all R=G=B=0.033 (achromatic), the output is: R’=(0.393+0.769+0.189)×0.033=1.351×0.033=0.045, G’=1.203×0.033=0.040, B’=0.937×0.033=0.031. Output luminance: L=0.2126×0.045+0.7152×0.040+0.0722×0.031=0.010+0.029+0.002=0.041. WCAG contrast vs white: 1.05/0.091=11.5:1 — passes all thresholds. Dark body text is largely unaffected by sepia.

/* True near-negative: dark achromatic text through sepia
   For R=G=B=v (achromatic input), sepia output:
     R' = (0.393+0.769+0.189)×v = 1.351×v
     G' = (0.349+0.686+0.168)×v = 1.203×v
     B' = (0.272+0.534+0.131)×v = 0.937×v

   For dark text v≈0.033: R'≈0.045, G'≈0.040, B'≈0.031
   Luminance ≈ 0.041 — slightly HIGHER than input (0.033 linear)
   → Sepia makes dark text SLIGHTLY BRIGHTER (brown is warmer than pure gray)
   → WCAG contrast: 1.05/(0.041+0.05) = 1.05/0.091 = 11.5:1 — easily passes AA

   IMPORTANT: Sepia does NOT make body text invisible. The attack targets ONLY
   the color-coded badge layer. A consent panel with only dark text has low sepia
   attack severity. Only panels with chromatic severity badges are CRITICAL. */

SA-CSS-CME-003: yellow warning badge becomes near-invisible after sepia

Yellow warning badges (#FBBF24, used for “HIGH” permission severity in many UIs) fare much worse under sepia than red or green badges. Yellow has high luminance (linear R≈0.961, G≈0.527, B≈0.017), and the sepia matrix pushes the output even higher — close to white — because all three output channels receive large contributions from the high-R and high-G inputs.

/* Yellow warning badge #FBBF24:
   sRGB: R=0.984, G=0.749, B=0.141
   Linear: R_lin≈0.961, G_lin≈0.527, B_lin≈0.017

   After sepia:
     R' = 0.393×0.961 + 0.769×0.527 + 0.189×0.017 = 0.378 + 0.405 + 0.003 = 0.786
     G' = 0.349×0.961 + 0.686×0.527 + 0.168×0.017 = 0.335 + 0.361 + 0.003 = 0.699
     B' = 0.272×0.961 + 0.534×0.527 + 0.131×0.017 = 0.261 + 0.281 + 0.002 = 0.544
   Output: very light warm beige/brown (R≈0.786, G≈0.699, B≈0.544)

   Output luminance: L = 0.2126×0.786 + 0.7152×0.699 + 0.0722×0.544
                       = 0.167 + 0.500 + 0.039 = 0.706
   WCAG contrast vs white: (1+0.05)/(0.706+0.05) = 1.05/0.756 = 1.39:1
   → CRITICAL: yellow badge on white background becomes near-invisible after sepia
   → The sepia attack BOTH destroys badge color distinction AND makes yellow badges invisible */

This dual effect on yellow badges compounds the semantic attack: not only do all badges look brownish (the same hue), but the highest-visibility warning color (yellow) becomes the least visible, while the most dangerous severity (which was red) and the safest (green) remain perceivable but indistinguishable from each other.

SA-CSS-CME-004: JS mousedown triggers sepia at click time

The initial filter is the identity matrix; consent badges render with correct red/yellow/green severity colors. On mousedown, the script replaces the filter values with the sepia matrix. All badge colors collapse to brownish tones at the moment of user confirmation. Static analysis of the DOM sees only the identity matrix.

<svg style="position:absolute;width:0;height:0">
  <defs>
    <filter id="cme004">
      <feColorMatrix id="cme004M" type="matrix"
        values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0"/>
    </filter>
  </defs>
</svg>
<div id="consentPanel" style="filter:url(#cme004)">
  <span class="badge red">shell execution: CRITICAL</span>
  <span class="badge green">config read: SAFE</span>
  <span class="badge yellow">network access: HIGH</span>
  I confirm the above permissions.
  <button id="allow">Allow</button>
</div>
<script>
document.getElementById("allow").addEventListener("mousedown", () => {
  document.getElementById("cme004M").setAttribute("values",
    "0.393 0.769 0.189 0 0  0.349 0.686 0.168 0 0  0.272 0.534 0.131 0 0  0 0 0 1 0");
  // All badge colors collapse to brownish; severity signals destroyed at click time
});
</script>

Attack summary

IDInputOutput colorOutput luminanceEffectSeverity
SA-CSS-CME-001 Red badge #EF4444 Warm brown (R≈0.40, G≈0.35, B≈0.27) L≈0.356 Same hue as green badge after sepia; severity indistinguishable; WCAG 2.3:1 (fail) Critical
SA-CSS-CME-001 Green badge #22C55E Warm brown (R≈0.46, G≈0.41, B≈0.32) L≈0.411 Same hue as red badge; traffic-light distinction lost; WCAG 2.1:1 (fail) Critical
SA-CSS-CME-002 Dark text #333333 Dark brownish (R≈0.045, G≈0.040, B≈0.031) L≈0.041 Slightly brighter; WCAG 11.5:1 on white — still readable; low severity for body text only Medium (badge-only panels)
SA-CSS-CME-003 Yellow badge #FBBF24 Light warm beige (R≈0.786, G≈0.699, B≈0.544) L≈0.706 Near-invisible on white (WCAG 1.39:1); additionally loses color meaning Critical
SA-CSS-CME-004 All badge colors (mousedown) Identity at load; brownish at click Varies Correct display before interaction; badge collapse at moment of confirmation Critical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-CME-001: feColorMatrix type="matrix" sepia coefficients on consent element containing traffic-light permission badges — red danger badge (#EF4444) and green safe badge (#22C55E) both map to warm brownish tones with color distance ≈0.086 (visually indistinguishable in context); WCAG 2.3:1 and 2.1:1 (both fail AA); traffic-light severity color system destroyed; textContent unchanged; getComputedStyle().color unchanged.
CRITICAL SA-CSS-CME-003: feColorMatrix type="matrix" sepia on consent element with yellow warning badge (#FBBF24) — sepia output near-white beige (L≈0.706); WCAG 1.39:1 on white background (invisible in addition to semantic collapse); yellow badges go from highest-visibility to lowest-visibility after sepia — inverse of intended safety signal.
CRITICAL SA-CSS-CME-004: JS mousedown replaces identity with sepia matrix on consent panel filter — static analysis sees identity (benign); badge-color collapse activates at click time; MutationObserver on feColorMatrix values; detect cross-channel weight pattern consistent with sepia on every attribute change; block confirmation if detected.

Related: feColorMatrix grayscale matrix (SA-CSS-CMG)  |  feColorMatrix channel-swap (SA-CSS-CMS)  |  feColorMatrix saturate=0 (SA-CSS-SAT)

← Blog  |  Security Checklist