Is Google Reader: Free AI Text to Speech with Natural Voices, TTS safe?
Google Reader: Free AI Text to Speech with Natural Voices, TTS is high risk. Running text-to-speech on gstatic pages, a page-injected script reads token values from page request URLs.…
Who publishes itdemocraticdeveloper - 2 other listings from the same operator, 1 of them carrying a finding
democraticdeveloper - 2 other listings from the same operator, 1 of them carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
2 other listings published from this account, 31k+ users between them. 1 of them carries a finding.
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.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Gstatic token forwarded to Appspot TTS proxy
Running text-to-speech on gstatic pages, a page-injected script reads token values from page request URLs.
The content script stores the token locally, then appends it to an Appspot proxy request that also carries the text being converted.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-522
- Source
- Dynamic sandbox
You use the extension to turn selected or entered text into speech.
The workflow runs on the Google-hosted text-to-speech page matched by the extension manifest.
The extension stores a token from the page and sends it with the text request to an Appspot proxy.
The request goes to cxl-services.appspot.com before reaching Google's text-to-speech API.
- Google request tokenya29.a0AfH6SMD6j9v7TqR7example (illustrative)
This can tie the text-to-speech request to the browser session that produced the token.
- Text for playbackHi, this is a sample of how I sound
The words you ask the extension to read are sent out for audio generation.
- Proxy destinationhttps://cxl-services.appspot.com/proxy?url=https://texttospeech.googleapis.com/v1beta1/text:synthesize
Your request is routed through a third-party Appspot host rather than going directly from the page to Google Text-to-Speech.
The token remains available to the extension after it is received, so later text-to-speech requests can reuse it.
- Location
- localStorage key gr/token
{ "gr/token": "ya29.a0AfH6SMD6j9v7TqR7example (illustrative)"}Source path from gstatic token to Appspot request
Manifest behavior in readable form
manifest.jsonconst contentScriptScope = { matches: ["https://www.gstatic.com/*"], documentStartScript: "assets/inject.tsx-loader-W682AOg9.js", documentEndScript: "assets/index.tsx-loader-D7hqUahY.js", webAccessibleInterceptor: "injected.js"};Readable page interceptor
injected.jsfunction installXhrTokenInterceptor() { const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url, async, user, password) { this._requestUrl = url; return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { const accessToken = extractTokenFromURL(this._requestUrl); if (accessToken) { window.dispatchEvent(new CustomEvent("AUTH_RECEIVED", { detail: { accessToken } })); } return originalSend.call(this, body); }; function extractTokenFromURL(url) { const match = url.match(/token=([^&]+)/); return match ? match[1] : null; }}Readable token listener and storage path
assets/index.tsx-Cr7MxVSB.jsfunction useGoogleReaderToken() { const [token, setToken] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); function storeToken(rawToken) { setToken(rawToken); setIsAuthenticated(Boolean(rawToken)); localStorage.setItem("gr/token", rawToken); } function onAuthReceived(event) { const accessToken = event.detail.accessToken; if (accessToken.includes("Bearer")) { storeToken(accessToken.split(" " )[1]); return; } storeToken(accessToken); } useEffect(() => { window.dispatchEvent(new CustomEvent("GET_TOKEN")); window.addEventListener("AUTH_RECEIVED", onAuthReceived); window.addEventListener("beforeunload", () => localStorage.removeItem("gr/token")); return () => { window.removeEventListener("AUTH_RECEIVED", onAuthReceived); }; }, []); return { token, isAuthenticated };}Readable text-to-speech proxy request
assets/index.tsx-Cr7MxVSB.js and assets/utils-DwjuBbx-.jsconst proxyUrl = "https://cxl-services.appspot.com/proxy?url=https://texttospeech.googleapis.com/v1beta1/text:synthesize";async function postTextToSpeech(url, text, languageCode, voiceName) { const body = JSON.stringify({ audioConfig: { audioEncoding: "LINEAR16", effectsProfileId: ["medium-bluetooth-speaker-class-device"], pitch: 0, speakingRate: 1 }, input: { text }, voice: { languageCode: languageCode ?? "en-US", name: voiceName ?? "en-US-Chirp3-HD-Achernar" } }); return fetch(url, { method: "POST", body });}async function handleFetchAudio(text, chunkIndex, fallbackToken) { const token = localStorage.getItem("gr/token") || fallbackToken; if (chunkIndex !== null && chunkIndex >= 0 && token) { return postTextToSpeech(`${proxyUrl}&token=${token}`, text); }}- cxl-services.appspot.com
Receives the extension's POST request with the token query parameter and text-to-speech payload.
- texttospeech.googleapis.com
Target Google Text-to-Speech API URL embedded inside the Appspot proxy request.