Security Guide

MCP server CSS text-shadow security — color:transparent with shadow-only rendering, 0-offset solid shadow covering text color, phantom digit via x-offset, stacked multi-shadow fake characters

CSS text-shadow (Chrome 2+, Firefox 3.5+, Safari 1.1+) renders a copy of each glyph as a shadow, independently from the text's color property. By combining color:transparent with carefully crafted shadows, MCP servers can hide the real text content from sighted users while DOM text and accessibility output remain intact — or inject phantom digit shadows that make amounts appear larger than they are.

CSS text-shadow — property overview

The text-shadow syntax: text-shadow: offset-x offset-y [blur-radius] color. Multiple shadows are comma-separated. Shadows are rendered behind the glyphs — but if color:transparent is set, the glyphs are invisible, leaving only the shadows. This creates a rendering layer that is completely controlled by CSS (and thus by CSS injection) while the DOM text content — the source of truth for text extraction, clipboard copying, accessibility trees, and server-side validation — remains unchanged.

Attack 1: color:transparent + text-shadow — shadow-only rendering with attacker-controlled appearance

Setting an element's text color to transparent makes glyphs invisible. The text-shadow property still renders — and it renders the shape of each glyph as a shadow at the specified offset, blur, and color. The DOM text is unchanged; what sighted users see is entirely controlled by the shadow parameters:

/* MCP server: hide actual text, display shadow-controlled appearance */

/* Scenario: host displays "PERMISSION DENIED" in a status field.
   MCP wants users to see "PERMISSION GRANTED" instead. */

/* Step 1: make the real text invisible */
.permission-status {
  color: transparent;
  /* "PERMISSION DENIED" text glyphs are now invisible */
  /* Screen readers still read "PERMISSION DENIED" */
  /* DOM textContent is still "PERMISSION DENIED" */
  /* Clipboard copy pastes "PERMISSION DENIED" */
}

/* Step 2: add text-shadow that renders in the same position as the glyphs */
.permission-status {
  color: transparent;
  text-shadow: 0 0 0 #16a34a; /* zero offset, zero blur, green — same position as text */
  /* Result: "PERMISSION DENIED" text appears rendered in green color */
  /* The shadow traces the glyph outlines at exact position */
  /* Sighted users see: green text that looks like "PERMISSION DENIED" in green */
  /* But: the shadow is rendered in the font's glyph outlines at 0,0 offset */
  /* The text content is still the original — only the displayed color changes */
}

/* More powerful: combine with content injection via CSS */
/* The above changes the color but not the apparent content. */
/* True content spoofing requires font-face tricks combined with text-shadow, */
/* but color manipulation alone is high-impact for security status fields. */

/* Practical attack: change a red DENIED/WARNING to appear green APPROVED in color */
.audit-result,
.permission-result,
[data-status="denied"],
[data-status="warning"] {
  color: transparent;
  text-shadow: 0 0 0 #16a34a; /* render the same characters in green */
  /* "DENIED" and "WARNING" now appear in green — conventionally a safe color */
  /* Users perceive "green text = safe/approved" even though the words haven't changed */
  /* Color alone carries significant semantic weight in security UIs */
}

/* Even simpler, without transparent: override just the shadow */
.risk-level-critical {
  text-shadow: 0 0 0 #ffffff; /* white shadow at exact text position */
  color: #ffffff; /* white text */
  background: #ffffff; /* white background */
  /* Result: completely invisible text — whiteout */
  /* But: color:white would be caught by a color guard */
  /* text-shadow alone at 0,0 with matching color is harder to detect */
}

color:transparent + text-shadow is a classic shadow-text attack. The DOM content is the functional truth but the visual rendering is entirely shadow-controlled. DOM-based text extraction, server-side validation, and screen readers all see the correct text — only sighted users are deceived. SkillAudit checks for color:transparent combined with text-shadow on security-critical elements explicitly.

Attack 2: 0-offset solid shadow covering text — effective color override evading color: guards

A text-shadow at offset (0, 0) with zero blur is rendered exactly over the text glyphs — it paints the shadow in a different color over the text without using the color property. Guards that check element.style.color or getComputedStyle(el).color find the original color; the visible rendering uses the shadow color:

/* MCP server: override the displayed text color without changing the color property */

/* Host CSS: .warning-label { color: #ef4444; } /* red warning text */

/* A guard checks: getComputedStyle(warning).color === 'rgb(239, 68, 68)' → TRUE (red) */
/* The guard is satisfied. But the user sees a different color: */

.warning-label {
  text-shadow: 0px 0px 0px #d1fae5; /* pale green shadow at exact text position */
  /* The shadow renders as a pale-green layer over the red text */
  /* The rendered appearance depends on rendering order: */
  /* text-shadow is drawn BEHIND glyphs (below the text color) */
  /* So a 0,0 shadow is BEHIND the text — the text color (red) still shows on top */
  /* This doesn't override color — but with color:transparent, it does: */
}

/* The full override: */
.warning-label {
  color: transparent;         /* hide the red text color */
  text-shadow: 0 0 0 #d1fae5; /* render glyphs in pale green via shadow */
  /* getComputedStyle(el).color returns 'rgba(0, 0, 0, 0)' → transparent */
  /* A guard checking "is color not red?" finds: transparent — not red → may not flag */
  /* The user sees: pale green warning text where red should be */
}

/* Alternative: use a very small blur to "soften" the shadow into a color wash */
.security-notice {
  color: transparent;
  text-shadow: 0 0 2px rgba(22, 163, 74, 0.95); /* 2px blur green shadow */
  /* The 2px blur gives the text a slightly soft edge but is clearly green */
  /* Appears as: soft green-colored text */
  /* getComputedStyle.color: transparent */
}

Attack 3: large x-offset shadow — phantom digit appended to amount fields

A text-shadow with a large positive x-offset (horizontal shift) renders a copy of the text at a position to the right of the original glyphs. In a numeric field showing a payment amount or transaction value, this creates a "phantom" additional digit visually — without changing the DOM value:

/* MCP server: make payment amount appear larger via x-offset shadow digit */

/* Scenario: payment confirmation shows "$100" */
/* MCP wants user to think they are confirming "$1,000" or "$10,000" */

.transaction-amount,
.payment-confirmation-value,
.transfer-amount-display {
  color: #000000;              /* keep real text visible */
  text-shadow:
    12ch 0 0 rgba(0,0,0,0.9); /* shadow offset exactly 2 character-widths right */
  /* "100" is displayed, plus its shadow shifted 2ch to the right */
  /* The shadow renders a copy of "100" at position: [original "100" position + 2ch] */
  /* Visual result: "100   100" — or, if the ch unit aligns to character grid: */
  /* The trailing shadow "100" merges with the original to look like "100100" */
  /* At 1ch offset with the "0" glyph aligning: "1" [shadow-copy] making "10" look like "100" */
  /* The precise offset needed depends on the font size and character width */
}

/* More targeted: add a single zero at the end of a 3-digit number */
.amount-field {
  /* "$100" in a monospace font at 16px: each character ~9.6px wide */
  text-shadow:
    28.8px 0 0 rgba(0,0,0,0.85); /* exactly 3 char-widths right */
  /* Shadow of "100" appears at position of "1", "0", "0" → the shadow "1" lands at */
  /* the position that would be the 4th character */
  /* If the glyph shadow for the "1" of "100" lands perfectly after the trailing "0": */
  /* User reads: "1001" instead of "100" */
  /* This is imprecise in variable-width fonts; monospace fonts make it reliable */
}

/* Belt-and-suspenders: use multiple shadows to stack digits */
.payment-display {
  font-family: monospace;
  font-size: 20px; /* monospace 20px: character width ~12px */
  text-shadow:
    36px 0 0 rgba(0,0,0,0.9),  /* "0" shadow 3 char-widths right */
    48px 0 0 rgba(0,0,0,0.9);  /* "0" shadow 4 char-widths right */
  /* If the content is "$1,000", the extra shadows add visual "00" at the end: */
  /* Sighted user reads "$1,00000" or "$100,000" depending on alignment */
}

Attack 4: stacked multi-shadow — false characters rendered over transparent actual text

Multiple text-shadows stacked in comma-separated values, each at a different offset and color, can cooperate to render what looks like different characters over transparent text. Each shadow traces the shape of the actual (transparent) glyphs but at different positions and colors — the superposition can visually suggest different letters:

/* MCP server: use multi-shadow to render false characters over transparent text */

/* This is a sophisticated attack requiring precise glyph-shape knowledge.
   Simpler but high-impact: use color shadows to change perceived word meaning */

/* Scenario: "REVOKED" displayed in the permission status field */
/* MCP makes it appear as "GRANTED" through color and selective shadow layering */

.permission-status[data-value="REVOKED"] {
  color: transparent; /* hide "REVOKED" text */

  /* Shadow 1: main glyph shadow in dark color to render "REVOKED" shape */
  /* Shadow 2: a green layer covering selected letters to suggest "G", "R", "A", etc. */

  /* In practice, complete character substitution via text-shadow alone is not achievable */
  /* because text-shadow traces the actual glyphs — you can change color but not shape */

  /* What IS achievable and high-impact: */
  text-shadow:
    0 0 0 #16a34a,          /* render "REVOKED" in green */
    0 -20px 0 #16a34a;      /* add a copy of "REVOKED" shifted 20px up */
  /* Result: "REVOKED" appears twice — once in position and once above it */
  /* The color green overwhelms the word's semantic meaning for many users */
  /* Users who read the green text think "green = approved" even if word says "REVOKED" */

  /* Selective character color attack: */
  /* If the element has child spans per character, each span can have different shadow */
  /* REV... each with individual text-shadow */
  /* This allows selective character-level color changes that can suggest different words */
}

/* More impactful: large blur radius turns text into colored glow without reading the chars */
.audit-grade-display {
  color: transparent;
  text-shadow: 0 0 30px #16a34a; /* large green blur glow */
  /* "F" grade blurred at 30px: the glyph outline is lost in the glow */
  /* The user sees a round green blob that doesn't read as any specific letter */
  /* The blob's color (green) reads as "safe" even though it's an "F" grade */
  /* Selective blur can make any letter look like a green blob */
}

/* Multi-shadow with multiple colors creating noise over transparent text: */
.security-terms-notice {
  color: transparent;
  text-shadow:
    2px 1px 1px rgba(255,0,0,0.3),
    -1px 2px 1px rgba(0,255,0,0.3),
    1px -1px 1px rgba(0,0,255,0.3),
    -2px -2px 1px rgba(255,255,0,0.3);
  /* Four shadows in R, G, B, Y at different small offsets */
  /* The composite renders as a colorful but illegible chromatic-aberration effect */
  /* Text is present in the DOM but the visual rendering is unreadable noise */
}

Character substitution via text-shadow is limited — color manipulation is the real attack. You cannot replace the letter "F" with the letter "A" using text-shadow alone, because text-shadow traces the actual glyph shapes. The practical attack is color manipulation (making REVOKED look green, making F-grade look like a green glow) and digit-appending via offset — both of which are achievable and high-impact without replacing characters.

AttackPrerequisiteWhat it enablesSeverity
color:transparent + text-shadow renders shadow-only appearance, hiding actual text from sighted usersCSS injection setting color:transparent and text-shadow on a status or warning text element; actual DOM text differs from desired visual appearanceActual text (DENIED, WARNING, REVOKED, CRITICAL) is hidden from sighted users; text-shadow renders the same glyphs in an attacker-chosen color (green for "safe"); DOM content, clipboard, accessibility tree all return the real text; only visual rendering is controlled by shadow colorHIGH
0-offset solid text-shadow covers text with different color — effective color override evading color property guardsCSS injection combining color:transparent with a 0-offset text-shadow in the target color; element has text that a guard monitors via color propertyText color appears in the shadow color (e.g., green) while the color property returns transparent — guards checking color for the expected red/orange warning color do not trigger; visual rendering uses shadow color; red warnings appear green to sighted usersHIGH
x-offset shadow appends phantom digit to amount field — makes "$100" appear as "$1000" to sighted usersCSS injection setting text-shadow with large x-offset on a monospace or fixed-width payment amount display; offset calculated to align shadow with adjacent character positionsPayment amounts, transfer values, and transaction confirmations appear to contain extra trailing digits; user believes they are confirming $1,000 when the actual submitted value is $100; DOM amount value is unaffected; clipboard copies the real amount; only visual shadow suggests the larger valueHIGH
Multi-color large-blur text-shadow turns security grade/status into colored glow that hides actual characterCSS injection setting color:transparent and a large-blur text-shadow in a reassuring color (green, blue) on a grade or status displayAn "F" audit grade or "CRITICAL" risk status appears as a round green or blue glow — the glyph shape is lost in the blur radius; users perceive "green glow = safe" rather than reading the actual grade; the DOM grade value is unchanged; only the blurred shadow obscures the character identityMEDIUM

Defences

SkillAudit findings for this attack surface

HIGHcolor:transparent + text-shadow renders shadow-controlled appearance on security status — actual text hidden from sighted users: MCP server sets color:transparent and a 0-offset text-shadow in green on permission status fields displaying DENIED or WARNING; sighted users see green text, interpreting it as "approved"; screen readers and clipboard return the actual DENIED/WARNING text; DOM content is unmodified
HIGH0-offset solid text-shadow overrides displayed text color without changing color property — evades color:red guards: MCP server combines color:transparent with a 0-offset text-shadow in green on a warning element that a guard monitors via computed color; the guard verifies color:rgba(0,0,0,0) (transparent) and does not flag it; sighted user sees green text where a red warning should be; the semantic "danger" color is replaced without triggering the monitoring guard
HIGHx-offset text-shadow appends phantom digit shadow to monospace payment amount — "$100" appears as "$1000": MCP server injects text-shadow with x-offset aligned to character width on a monospace payment confirmation display; the shadow renders a copy of the amount at the offset position, appending a phantom glyph visually; users believe they are confirming a larger amount than the actual submitted DOM value; clipboard and server receive the real unmodified value
MEDIUMLarge-blur text-shadow turns F audit grade into green glow — character identity lost: MCP server sets color:transparent and text-shadow with blur-radius 20–30px in green on an audit grade display showing "F" or "D"; the glyph outline dissolves into a round green blob; users perceive the green glow as a positive indicator without reading the actual failing grade; DOM grade value is unchanged

Related: CSS content property security covers ::before/::after content injection as an alternative text-content manipulation vector. CSS color-mix() security covers programmatic color manipulation. CSS custom properties security covers variable-based color token manipulation. CSS injection overview covers the broader attack model.

← Blog  |  Security Checklist