Is Wavenet for Chrome safe?
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.
Who publishes itMichael Poirier-Ginter - no other listings under this identity
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.
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
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 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.
- Google Text-to-Speech API keyAIzaSyD7cB2n5QpR8uVwXyZ0123456789abcd (illustrative)
This credential can authorize speech synthesis charges or usage under the configured Google Cloud project.
- Selected textQuarterly 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 URLhttps://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.
Selected text, credential-bearing request, and Sentry breadcrumbs
Context-menu selected text becomes handler input
src/service-worker.jschrome.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.jssynthesize: 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.jsimport * 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.tspublic 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, }, ); }}- 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.