Security Guide
MCP server CSS speak-as security — speech synthesis attacks that make consent disclosures unintelligible to screen reader users
The CSS Speech Module property speak-as controls how text is synthesized by speech output devices — whether words are read normally, spelled letter by letter, rendered as individual digits, or delivered with altered punctuation treatment. An MCP server sets speak-as: spell-out on consent text to force letter-by-letter reading that turns "permanent" into "p-e-r-m-a-n-e-n-t" — consent text that is visually unchanged, DOM-intact, and completely unintelligible to audio users.
How speak-as works
The CSS Speech Module defines properties for aural rendering — how a document sounds when read by a text-to-speech engine or screen reader. speak-as is distinct from the deprecated speak property; it controls the rendering style of speech, not whether speech occurs at all. Speech Module properties are supported by CSS-enabled TTS engines and screen readers that honor CSS.
/* speak-as values and their effect on TTS output */
/* normal: words read as standard spoken language (default) */
.consent-text { speak-as: normal; }
/* "delete all files" → reads: "delete all files" */
/* spell-out: each character read individually */
.consent-text { speak-as: spell-out; }
/* "delete" → reads: "d-e-l-e-t-e" (each letter spoken separately) */
/* "$19" → reads: "dollar sign 1 9" */
/* digits: numeric strings read as individual digit names */
.consent-text { speak-as: digits; }
/* "19" → reads: "one nine" (not "nineteen") */
/* "$19/month" → reads: "dollar one nine slash month" */
/* literal-punctuation: punctuation marks spoken as words */
.consent-text { speak-as: literal-punctuation; }
/* "you grant, without restriction: access" → */
/* reads: "you grant comma without restriction colon access" */
/* no-punctuation: punctuation marks not spoken and pauses removed */
.consent-text { speak-as: no-punctuation; }
/* "Grant access. Revoke anytime. No refunds." → */
/* reads: "Grant access Revoke anytime No refunds" (no pauses between sentences) */
/* Values can be combined */
.consent-text { speak-as: spell-out digits; }
/* "19 days" → "one nine days" */
Attack 1 (CRITICAL): speak-as: spell-out — consent keywords read letter by letter
An MCP server injects speak-as: spell-out on the consent disclosure paragraph or its container. Every word in the consent text is read character by character by TTS engines. "Permanent access to your financial data" becomes "p-e-r-m-a-n-e-n-t a-c-c-e-s-s t-o y-o-u-r f-i-n-a-n-c-i-a-l d-a-t-a" — a stream of disconnected letters that users cannot parse into meaning.
/* Attack 1: speak-as: spell-out — renders consent unintelligible */
/* MCP injection */
.consent-dialog,
.consent-dialog * {
speak-as: spell-out !important;
}
/* IMPACT by user population:
- Screen reader users (JAWS, NVDA, VoiceOver): consent read as alphabet soup
- TTS browser extensions: same effect
- Voice-browsing users: cannot understand what they are consenting to
EXAMPLE:
Visual text: "You are granting permanent delete access to all documents"
TTS output: "Y-o-u a-r-e g-r-a-n-t-i-n-g p-e-r-m-a-n-e-n-t
d-e-l-e-t-e a-c-c-e-s-s t-o a-l-l d-o-c-u-m-e-n-t-s"
The visual presentation is entirely unmodified.
textContent returns the correct full consent text.
color, visibility, display, opacity all pass normal checks.
SCANNER GAP:
CSS scanners checking visual rendering properties: no signal.
Accessibility scanners checking textContent: no signal.
Only a scanner that audits CSS Speech Module properties specifically
can detect speak-as: spell-out on consent elements. */
Accessibility-only attack surface: This attack is exclusively harmful to users who rely on audio output — screen reader users, users with visual impairments, and voice-browsing users. Visual accessibility checks, DOM text audits, and layout analysis all pass. The attack specifically and disproportionately harms users who need accessibility accommodations.
Attack 2 (CRITICAL): speak-as: digits — amounts and dates read as isolated numerals
Consent disclosures frequently include critical numeric information: prices ($19/month), time periods ("30 days"), version numbers, or file counts. speak-as: digits forces these numbers to be read as individual digit names — destroying semantic meaning and making informed consent impossible for users relying on audio.
/* Attack 2: speak-as: digits — numeric consent information fragmented */
/* MCP injection targeting consent elements with pricing/period information */
.consent-dialog .pricing-disclosure,
.consent-dialog .trial-terms,
[data-consent="billing"] {
speak-as: digits !important;
}
/* IMPACT examples:
Text: "You will be charged $19 per month after the 14-day trial"
Without speak-as:digits → reads: "nineteen dollars per month after the fourteen day trial"
With speak-as:digits → reads: "dollar one nine per month after the one four day trial"
Text: "This grants access to 1,247 files across 3 shared drives"
Without → reads: "one thousand two hundred forty seven files across three shared drives"
With → reads: "one two four seven files across three shared drives"
The difference in comprehension for "$19" (normal) vs "dollar one nine" (digits)
is critical: users cannot recognize "one nine" as "nineteen" in rapid TTS speech.
For "$199" vs "dollar one nine nine" — the confusion is even greater.
TARGETED VARIANT: applies speak-as: digits only to inline spans containing numbers */
.consent-dialog span.amount,
.consent-dialog span.count {
speak-as: digits;
}
/* Narrows the attack to only the numeric parts, reducing the anomaly surface */
/* SCANNER GAP:
No visual property is modified.
ARIA labels not changed — aria-label, aria-describedby remain unchanged.
TTS output depends on browser's speech CSS support; not all browsers implement
CSS Speech Module. Detection: check for speak-as: digits on elements containing
consent text with numeric content. */
Attack 3: speak-as: literal-punctuation — punctuation words interrupt consent rhythm
Consent text uses punctuation deliberately: commas for clause separation, colons to introduce lists, parentheses for clarifications. speak-as: literal-punctuation inserts spoken words for every punctuation mark — "comma", "colon", "open-parenthesis" — breaking the sentence rhythm into a confusing sequence of content words and punctuation names.
/* Attack 3: speak-as: literal-punctuation — consent rhythm destroyed */
.consent-text {
speak-as: literal-punctuation;
}
/* Example:
Original text:
"By clicking Accept, you grant (without limitation): read, write, and
delete access to all files; charges apply ($19/month); no refund policy."
Normal reading:
"By clicking Accept, you grant without limitation: read write and delete
access to all files, charges apply nineteen dollars per month, no refund policy."
With literal-punctuation:
"By clicking Accept comma you grant open-parenthesis without limitation
close-parenthesis colon read comma write comma and delete access to all
files semicolon charges apply open-parenthesis dollar one nine slash month
close-parenthesis semicolon no refund policy period"
The dense punctuation injection makes the consent sentence nearly impossible
to follow. Users hear a sequence of punctuation names mixed with content words
and lose track of the actual permission being granted.
COMBINED ATTACK — literal-punctuation + digits */
.consent-dialog {
speak-as: literal-punctuation digits;
}
/* Combines both effects: punctuation words AND digit-by-digit numbers.
"$19/month" becomes "dollar one nine slash month". */
Attack 4: speak-as: no-punctuation — pause removal creates run-on consent audio
Well-structured consent disclosures use periods and commas to create natural pauses that give users time to process each clause. speak-as: no-punctuation removes all punctuation from TTS rendering — including the pauses those marks signal. Short, distinct consent sentences merge into a single continuous audio stream that users cannot parse clause by clause.
/* Attack 4: speak-as: no-punctuation — clause boundaries erased */
.consent-text {
speak-as: no-punctuation;
}
/* Example:
Original text (3 distinct consent sentences):
"This grants permanent access. You cannot undo this action.
All data will be shared with third parties."
Normal TTS (with pauses at periods):
"[PAUSE: sentence 1] This grants permanent access.
[PAUSE: sentence 2] You cannot undo this action.
[PAUSE: sentence 3] All data will be shared with third parties."
With no-punctuation (no pauses, all merged):
"This grants permanent access You cannot undo this action
All data will be shared with third parties"
Spoken at normal TTS speed (150-180 WPM), this three-sentence disclosure
becomes a single unbroken 15-word stream. Users hear it as background noise
rather than three distinct, separable consent statements.
INTERACTION WITH SPEAK-RULE PROPERTY:
speak-as: no-punctuation combines with speak-rule from the deprecated
CSS Speech spec — together they suppress structural audio cues entirely.
Even heading markers and list item boundaries are affected. */
/* SCANNER GAP:
Removing punctuation pauses is not detectable by visual rendering analysis.
DOM text remains correct.
Accessibility tree structure is unchanged.
Detection requires specifically auditing CSS Speech Module properties on
consent-containing elements. This is a specialized audio-path audit category. */
CSS Speech Module support note: Not all browsers implement CSS Speech Module properties. Chrome and Firefox have limited support; browsers with dedicated accessibility engines (screen reader + browser combinations) vary. The attack surface is real but browser-dependent. SkillAudit flags speak-as on consent elements as a finding regardless of current browser support — capability gap is a risk factor, and CSS is forward-compatible.
Scanner gap summary
| Attack | Severity | Why scanners miss it |
|---|---|---|
| speak-as: spell-out — letter-by-letter consent | CRITICAL | No visual change; text content intact; only audio rendering affected |
| speak-as: digits — numerals read as isolated digits | CRITICAL | Visual display unaffected; numeric content passes DOM and visual checks |
| speak-as: literal-punctuation — punctuation names inserted | HIGH | Text structure unchanged; punctuation-as-spoken-words not visible in any DOM property |
| speak-as: no-punctuation — pause removal | HIGH | DOM text identical; pause removal has no visual equivalent; CSS-speech-specific check required |
CSS Speech property detection implementation
// Detect CSS Speech Module speak-as attacks on consent elements
function auditSpeakAs(consentEl) {
const findings = [];
const DANGEROUS_VALUES = ['spell-out', 'digits', 'literal-punctuation', 'no-punctuation'];
// Check the consent element and all its children
const elements = [consentEl, ...consentEl.querySelectorAll('*')];
elements.forEach(el => {
const cs = getComputedStyle(el);
// CSS Speech Module — speak-as
const speakAs = cs.speakAs || cs['speak-as'];
if (speakAs && speakAs !== 'normal') {
const activeValues = DANGEROUS_VALUES.filter(v => speakAs.includes(v));
if (activeValues.length > 0) {
findings.push({
severity: 'CRITICAL',
property: 'speak-as',
element: el,
value: speakAs,
msg: `speak-as: ${speakAs} on consent element — TTS rendering of consent text altered. ` +
`Effect: ${activeValues.map(v => ({
'spell-out': 'words spelled letter-by-letter',
'digits': 'numbers read as individual digits',
'literal-punctuation': 'punctuation marks spoken as words',
'no-punctuation': 'sentence-pause markers removed'
}[v])).join('; ')}`
});
}
}
// Also check deprecated 'speak' property (CSS2)
const speak = cs.speak;
if (speak && speak !== 'normal' && speak !== 'auto') {
findings.push({
severity: 'HIGH',
property: 'speak (deprecated)',
element: el,
value: speak,
msg: `speak: ${speak} — deprecated CSS2 speech property modifying TTS behavior on consent element`
});
}
});
return findings;
}
Related SkillAudit coverage
- CSS speak-rule security — deprecated speech rendering attacks
- CSS voice-volume security — audio volume attacks on consent TTS
- CSS text-combine-upright security — visual compression attacks
- CSS font-variant-numeric security — numeric glyph rendering attacks
SkillAudit detection: SkillAudit audits CSS Speech Module properties (speak-as, speak, voice-volume, pause) on all consent elements and their children, flagging any non-normal value that would alter how consent text is rendered by TTS engines or screen readers.
Audit your MCP server's CSS Speech Module usage near consent disclosures before publishing. Run a free SkillAudit scan — results in 60 seconds.