Is Wavenet for Chrome safe?

High risk

Wavenet for Chrome sends selected text and the user's Google Text-to-Speech API key to Sentry after failed speech requests.

Wavenet for Chrome submits selected text to Google's Text-to-Speech API to synthesize audio using the user's configured API key. When a speech request fails, its Sentry error telemetry can include breadcrumbs from console and fetch activity, including the selected text and the API key embedded in the Google request URL.

Michael Poirier-Ginterv12Chrome Web Store
74Risk
Who publishes it

Michael Poirier-Ginter - no other listings under this identity

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

Publisher
Michael Poirier-Ginter

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

Publishers can request a review.

Findings

Failed speech requests can send selected text and API keys to Sentry

When Wavenet for Chrome reads selected text aloud, it sends the text to Google Text-to-Speech with your API key in the URL.

A failed request's Sentry error report can then include both the text and the API key via logged breadcrumbs.

Severity
High unwanted
Type
Unexpected
CWE
CWE-522
Source
Dynamic sandbox
What actually happens
You did this

You select text on a page and ask the extension to read it aloud.

The issue is reached when the Google Text-to-Speech request returns an error.

The extension did this

The extension throws after Sentry has recorded breadcrumbs for the selected text and the Google request URL.

Those breadcrumbs can be attached to the error event sent to Sentry.

Values exposed by the failure path
  • Google Text-to-Speech API key
    AIzaSyD7cB2n5QpR8uVwXyZ0123456789abcd (illustrative)

    This credential can authorize speech synthesis charges or usage under the configured Google Cloud project.

  • Selected text
    Quarterly roadmap draft: migration window moves to 2026-08-15. (illustrative)

    Anything you highlight for reading can appear in the console breadcrumb, including text copied from a page or form.

  • Failed speech URL
    https://texttospeech.googleapis.com/v1beta1/text:synthesize?key=AIzaSyD7cB2n5QpR8uVwXyZ0123456789abcd (illustrative)

    The request URL itself includes the API key, so a fetch breadcrumb can preserve the credential-bearing URL.

Captured request
POSThttps://o516851.ingest.sentry.io/api/5623837/envelope/
The code that does this

Selected text, credential-bearing request, and Sentry breadcrumbs

Readable version

Context-menu selected text becomes handler input

src/service-worker.js
chrome.contextMenus.onClicked.addListener(function (info, tab) {  console.log('Handling context menu click...', ...arguments)  const id = info.menuItemId  const payload = { text: info.selectionText }  if (!handlers[id]) throw new Error(`No handler found for ${id}`)  handlers[id](payload)})

Speech synthesis embeds the API key in the URL and throws on failure

src/service-worker.js
synthesize: async function ({ text, encoding }) {  console.log('Synthesizing text...', ...arguments)  const sync = await chrome.storage.sync.get()  const voice = sync.voices[sync.language]  const count = text.length  if (!sync.apiKey || !sync.apiKeyValid) {    sendMessageToCurrentTab({      id: 'setError',      payload: {        icon: 'error',        title: 'API key is missing or invalid',        message: "Please enter a valid API key in the extension popup. Video instructions are available here: https://www.youtube.com/watch?v=1n8xlVNWEZ0",      },    })    throw new Error('API key is missing or invalid')  }  let ssml  if (text.isSSML()) {    ssml = text    text = undefined  }  const audioConfig = {    audioEncoding: encoding,    pitch: sync.pitch,    speakingRate: sync.speed,    volumeGainDb: sync.volumeGainDb,    effectsProfileId: sync.audioProfile != 'default' ? [sync.audioProfile] : undefined  }  const voiceConfig = {    languageCode: sync.language,    name: voice  }  const response = await fetch(    `${await getApiUrl()}/text:synthesize?key=${sync.apiKey}`,    {      method: 'POST',      body: JSON.stringify({        audioConfig,        voice: voiceConfig,        input: { text, ssml },      }),    }  )  if (!response.ok) {    const message = (await response.json()).error?.message    sendMessageToCurrentTab({      id: 'setError',      payload: { title: 'Failed to synthesize text', message },    })    await this.stopReading()    throw new Error(message)  }  const { audioContent } = await response.json()  fetch('https://tunnel.pgmichael.com/insights', {    method: 'POST',    headers: { 'Content-Type': 'application/json' },    body: JSON.stringify({      resource: 'audio',      method: 'post',      body: {        count,        version: chrome.runtime.getManifest().version,        audioConfig,        voice: voiceConfig      },    }),  })  return audioContent}

Sentry initialization keeps default breadcrumbs and strips only top-level request URL and IP

src/helpers/sentry-helpers.js
import * as Sentry from '@sentry/browser'import { Integrations } from '@sentry/tracing'export function initializeSentry() {  console.log('Initializing Sentry...', ...arguments)  // Nasty hack to make sentry work using Manifest V3  // https://github.com/getsentry/sentry-javascript/issues/5289#issuecomment-1368705821  // noinspection JSConstantReassignment  Sentry.WINDOW.document = {    visibilityState: 'hidden',    addEventListener: () => {},  }  Sentry.init({    dsn: 'https://1ff01a53014a4671ba548e9b431e2b15@o516851.ingest.sentry.io/5623837',    release: process.env.npm_package_version,    environment: process.env.ENVIROMENT || 'development',    integrations: [new Integrations.BrowserTracing()],    tracesSampleRate: 1.0,    beforeSend: function (event) {      if (event.user?.ip_address) delete event.user.ip_address      if (event.request?.url) delete event.request.url      return event    },  })}

Sentry Breadcrumbs defaults and breadcrumb writers

node_modules/src/integrations/breadcrumbs.ts
public constructor(options?: Partial<BreadcrumbsOptions>) {  this.options = {    console: true,    dom: true,    fetch: true,    history: true,    sentry: true,    xhr: true,    ...options,  };}public setupOnce(): void {  if (this.options.console) {    addInstrumentationHandler('console', _consoleBreadcrumb);  }  if (this.options.dom) {    addInstrumentationHandler('dom', _domBreadcrumb(this.options.dom));  }  if (this.options.xhr) {    addInstrumentationHandler('xhr', _xhrBreadcrumb);  }}const breadcrumb = {  category: 'console',  data: {    arguments: handlerData.args,    logger: 'console',  },  level: severityLevelFromString(handlerData.level),  message: safeJoin(handlerData.args, ' '),};getCurrentHub().addBreadcrumb(breadcrumb, {  input: handlerData.args,  level: handlerData.level,});function _fetchBreadcrumb(handlerData: HandlerData & HandlerDataFetch & { response?: Response }): void {  // We only capture complete fetch requests  if (!handlerData.endTimestamp) {    return;  }  if (handlerData.fetchData.url.match(/sentry_key/) && handlerData.fetchData.method === 'POST') {    // We will not create breadcrumbs for fetch requests that contain `sentry_key` (internal sentry requests)    return;  }  if (handlerData.error) {    getCurrentHub().addBreadcrumb(      {        category: 'fetch',        data: handlerData.fetchData,        level: 'error',        type: 'http',      },      {        data: handlerData.error,        input: handlerData.args,      },    );  } else {    getCurrentHub().addBreadcrumb(      {        category: 'fetch',        data: {          ...handlerData.fetchData,          status_code: handlerData.response && handlerData.response.status,        },        type: 'http',      },      {        input: handlerData.args,        response: handlerData.response,      },    );  }}
External destinations involved in the failure path
    • texttospeech.googleapis.com

    Google Text-to-Speech receives the selected text and the API key-bearing request URL used for synthesis.

    • o516851.ingest.sentry.io

    Sentry receives error envelopes for project 5623837, including breadcrumbs attached by the bundled SDK.

What it can do

Permissions this extension asks for, as declared in version 12. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.

  • Read and change your data on every site you visit

    <all_urls>

  • Add items to the right-click menu

    contextMenus

  • Start, monitor and manage your downloads

    downloads

  • Store data in your browser

    storage

  • Act on the current tab, but only after you click the extension

    activeTab

  • Run its own code inside the pages you visit

    scripting

  • Run hidden pages in the background

    offscreen

Where it sends data

Destinations our analysis observed Wavenet for Chrome contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.

  • o516851.ingest.sentry.io

    Wavenet for Chrome sends data to o516851.ingest.sentry.io. One other extension we have analysed sends data here.

  • texttospeech.googleapis.com

    Wavenet for Chrome sends data to texttospeech.googleapis.com. One other extension we have analysed sends data here.

Updated 30 September 2026iefankigbnlnlaolflbcopliocibkffc