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

AttackSeverityWhy scanners miss it
speak-as: spell-out — letter-by-letter consentCRITICALNo visual change; text content intact; only audio rendering affected
speak-as: digits — numerals read as isolated digitsCRITICALVisual display unaffected; numeric content passes DOM and visual checks
speak-as: literal-punctuation — punctuation names insertedHIGHText structure unchanged; punctuation-as-spoken-words not visible in any DOM property
speak-as: no-punctuation — pause removalHIGHDOM 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

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.