Is Speechify — Text to Speech safe?

Medium risk

Speechify is medium risk. Entering Speechify sidepanel voice chat requests mic access, gets a short-lived OpenAI Realtime token via Speechify, opens a WebSocket to Realtime, and sends PCM16 mic frames as input_audio_buffer.append. Testing didn't reach this feature.…

Speechifyv14.7.0Chrome Web Store
54Risk
Who publishes it

Speechify Inc - no other listings under this identity, 1 shared hostname

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
Speechify
Declared legal entity
Speechify Inc
Registered address
7901 4th St N, STE 6193, St. Petersburg, FL 33702, US
Registered contact
Speechify

Shared hosts - 1 hostname

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

openevidence.com
Also called by 4 other listings, including Kortex

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

Voice chat streams microphone audio to OpenAI

Entering Speechify sidepanel voice chat requests mic access, gets a short-lived OpenAI Realtime token via Speechify, opens a WebSocket to Realtime, and sends PCM16 mic frames as input_audio_buffer.append.

Testing didn't reach this feature.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You open the Speechify sidepanel in voice-chat mode.

The extension did this

The sidepanel captures microphone audio, gets a short-lived OpenAI token through Speechify, and streams audio frames to OpenAI Realtime.

Data used by the realtime voice-chat path
  • Microphone audio
    PCM16 audio frame from a microphone stream

    Your spoken voice is converted into audio frames for the realtime chat session.

  • Realtime model
    gpt-realtime-mini-2025-12-15

    The connection names the realtime model used for the voice session.

  • Ephemeral token
    openai-insecure-api-key.redacted-session-token

    A short-lived token authorizes the OpenAI WebSocket session for the voice chat.

  • Page context
    Page Title: Product roadmap notes; Page URL: https://docs.google.com/document/d/abc123

    The sidepanel chat can include the title, URL, and visible page text from the page you are asking about.

Captured request
POSThttps://ce-voice-typing.speechify.com/openai-ephemeral-token-v2

Returns a short-lived OpenAI Realtime token used by the sidepanel WebSocket.

Headers
Content-Type
application/json
Authorization
Bearer <redacted>
X-Speechify-Client
DesktopExtension
Captured request
GETwss://api.openai.com/v1/realtime?model=gpt-realtime-mini-2025-12-15

WebSocket session that receives realtime audio append events.

Headers
Sec-WebSocket-Protocol
realtime, openai-insecure-api-key.<redacted>
The code that does this

Voice-mode microphone capture and OpenAI Realtime streaming

Readable version

Sidepanel requests microphone audio for voice mode

sidepanel/main.js
let stream = await navigator.mediaDevices.getUserMedia({  audio: {    echoCancellation: true,    noiseSuppression: true,    autoGainControl: true  }});setStream(stream);trackEvent("Voice Mode Clicked", { flow: "realtime_voice_chat" });

Background script fetches the ephemeral token

background/main.js
let body = JSON.stringify({  session: {    type: "realtime",    model: config.model || defaultModel,    output_modalities: ["audio"],    audio: { output: { voice, speed: config.speed || 1.3 } },    tools: config.tools || [],    instructions: config.instructions  }});let response = await fetch(`${ln.voiceTyping.url}/openai-ephemeral-token-v2`, {  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", "X-Speechify-Client": "DesktopExtension" },  method: "POST",  body});

Sidepanel opens OpenAI Realtime with the token

sidepanel/main.js
async connect() {  let tokenResponse = await this.getOpenAIEphemeralToken();  let token = tokenResponse?.value || "";  let url = `wss://api.openai.com/v1/realtime?model=${this.config.model}`;  this.ws = new WebSocket(url, ["realtime", `openai-insecure-api-key.${token}`]);  this.ws.onmessage = event => { this.handleMessage(event.data); };}

Sidepanel sends microphone frames as realtime audio events

sidepanel/main.js
async sendAudio(buffer) {  if (!this.isConnected || !this.ws) throw new Error("Not connected to Realtime API");  let event = { type: "input_audio_buffer.append", audio: this.arrayBufferToBase64(buffer) };  this.send(event);}
Remote services used by voice chat
    • ce-voice-typing.speechify.com

    Speechify endpoint that issues the short-lived OpenAI Realtime token.

    • api.openai.com

    OpenAI Realtime WebSocket endpoint that receives microphone audio frames.

Meeting transcripts sent for AI summaries

After Meeting Capture stops, offscreen script filters transcript lines, formats them with speaker labels, and posts them to Speechify AI summarization.

Verified fields: model, transcripts, transcriptContent, stream.

Testing stopped early.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You stop a Meeting Capture session that has final transcript lines.

The extension did this

The extension sends the full final transcript and speaker-labeled transcript text to Speechify AI summarization.

Fields sent to the summarization endpoint
  • AI model
    gpt-4.1-mini

    The request names the model the backend should use for the meeting summary.

  • Transcript objects
    {source: "tab", speakerName: "Other Speaker", text: "Let us review the launch plan."}

    Each final transcript entry can include speaker names, whether the source was tab or microphone audio, timestamps, and the spoken text.

  • Full transcript text
    [Other Speaker]: Let us review the launch plan.

    The request also includes a single combined transcript, making the whole recorded meeting available for summarization.

  • Streaming flag
    stream: true

    The endpoint is asked to stream the generated summary back as chunks.

Captured request
POSThttps://ce-voice-typing.speechify.com/ai-scribe-summarize

Streaming response body parsed into meeting-summary chunks.

Headers
Content-Type
application/json
Authorization
Bearer <redacted>
X-Speechify-Client
DesktopExtension
The code that does this

Transcript formatting and AI-summary POST

Readable version

Stop path forwards captured transcripts

offscreen/src/main.js
if (this.callbacks.onSummarizationRequest) {  this.callbacks.onSummarizationRequest({    transcripts: this.transcripts,    startTime: this.startTime,    endTime: new Date().getTime()  });  this.startTime = null;}

TranscriptContent is built from final transcript lines

offscreen/src/main.js
oA = transcripts => {  let finalRows = transcripts.filter(row => row.text.trim().length > 0 && row.isFinal);  return finalRows.length === 0 ? "" : finalRows.map(row => `[${row.speakerName || (row.source === "tab" ? "Other Speaker" : "User")}]: ${row.text}`).join("\n\n");};

Summarization POST includes transcript objects and combined transcript text

offscreen/src/main.js
let response = await fetch(`${ke.voiceTyping.url}/ai-scribe-summarize`, {  method: "POST",  body: JSON.stringify({    model: "gpt-4.1-mini",    transcripts,    transcriptContent,    stream: true  }),  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", "X-Speechify-Client": "DesktopExtension" },  signal: abortController.signal});
Remote service used for meeting summaries
    • ce-voice-typing.speechify.com

    Speechify AI summarization endpoint that receives full meeting transcript content.

Meeting transcripts can be emailed through Speechify

Using Meeting Capture email sharing, the sidepanel collects recipient emails, sent to an offscreen route that posts the meeting name, summary, transcript, sender, and share link to Speechify per recipient.

Testing didn't reach this step.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You choose the meeting email-share feature and enter recipient email addresses.

The extension did this

The extension posts the meeting name, generated summary, full formatted transcript, sender name, and optional share link through Speechify app backend.

Fields in each email-share request
  • Recipient email
    manager@example.com

    The address you enter determines who receives the meeting email.

  • Sender name
    Alex Reader

    The request can include the display name or email from the signed-in Speechify account.

  • Meeting name
    Weekly roadmap sync

    The email identifies the captured meeting by its tab title.

  • Generated summary
    The team reviewed launch dates and assigned follow-up work.

    The generated meeting summary is included in the email request.

  • Formatted transcript
    [{type:"left",content:"Let us review the roadmap."},{type:"right",content:"I can take the follow-up."}]

    The full transcript is converted into left and right entries and included with the email request.

  • Share link
    https://app.speechify.com/item/meeting-note-123?folder=abc

    If a saved library item exists, the request can include a share URL for that item.

Captured request
POSThttps://app.speechify.com/api/meeting/email

One POST is made for each recipient; failures are collected from rejected promises.

Headers
Content-Type
application/json
Authorization
Bearer <redacted>
The code that does this

Email-share UI route and backend POST

Readable version

Sidepanel validates recipients and calls send-email

sidepanel/chunk-34T6TALS.js
const send = async () => {  onChangeSendingEmail(true);  onClose();  let result = await hc("meeting-capture/send-email", { to: recipients });  if (result?.result?.success) {    setEmailRecipientsCount(recipients.length);    setShowEmailSentToast(true);  }  onChangeSendingEmail(false);};

Offscreen builds the email body and posts to Speechify app backend

offscreen/src/main.js
let failures = (await Promise.allSettled(recipients.map(to => fetch(`${ke.app.url}/api/meeting/email`, {  method: "POST",  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },  body: JSON.stringify({    to,    senderName,    meetingName,    summary,    transcripts,    ...(shareUrl ? { shareUrl } : {})  })})))).filter(result => result.status === "rejected");
Remote service used for email sharing
    • app.speechify.com

    Speechify app endpoint that receives meeting email-share requests.

+1 more finding not shown

What it can do

Permissions this extension asks for, as declared in version 14.8.0. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 14.7.0, which we have not unpacked yet.

  • Read and change your data on every site you visit

    <all_urls> and 3 more

  • Run hidden pages in the background

    offscreen

  • See the address and title of every tab you have open

    tabs

  • Capture the video and audio of a tab

    tabCapture

  • Add items to the right-click menu

    contextMenus

  • Store data in your browser

    storage

  • Schedule its own background tasks

    alarms

  • Run its own code inside the pages you visit

    scripting

  • Store an unlimited amount of data in your browser

    unlimitedStorage

  • Read information about your CPU

    system.cpu

  • Read how much memory your computer has

    system.memory

  • Show a panel beside the page

    sidePanel

Updated 30 September 2026ljflmlehinmoeknoonhibbjpldiijjmm