Is YoutubeDigest: summarize and translate using ChatGPT safe?
YoutubeDigest is medium risk. Signing in with Google or Twitter, the extension reads the OAuth code from the redirect and POSTs it to youtubedigest.app/api/exchange, which returns access and refresh tokens instead of the OAuth provider doing so directly.…
Who publishes ittheaibigfan - no other listings under this identity, 2 shared hostnames
theaibigfan - no other listings under this identity, 2 shared hostnames
What this publisher told the store about itself, and the other listings that told it the same thing.
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
OAuth codes are exchanged through youtubedigest.app
Signing in with Google or Twitter, the extension reads the OAuth code from the redirect and POSTs it to youtubedigest.app/api/exchange, which returns access and refresh tokens instead of the OAuth provider doing so directly.
You sign in with Google or Twitter through the extension.
The flow uses the browser identity API to open the provider's OAuth page.
The extension sends the returned authorization code to youtubedigest.app for token exchange.
The response is stored locally as access and refresh tokens for later authenticated requests.
| Field | Value | Why it matters | |
|---|---|---|---|
OAuth authorization code | 4/0AeaYSHB7eZQ2ExampleAuthCodeForSignIn | This short-lived value is exchanged for tokens that let the extension act as your signed-in account within the requested scope. | |
Access token | ya29.a0AfH6SMAExampleAccessTokenValue | This token lets the extension make authenticated requests after the sign-in flow completes. | |
Refresh token | 1//0gExampleRefreshTokenForOfflineAccess | This token can be used to obtain new access tokens after the original access token expires. | |
Signed-in account identity | alex@example.com | The extension stores account details so later requests can be tied to your signed-in profile. |
| Content-Type | application/json |
Google OAuth code is posted to youtubedigest.app
var jn = "1015583372301-ifdpu597snn6mtkveq81da9fsk3jmof7.apps.googleusercontent.com",
Dn = "https://www.googleapis.com/auth/userinfo.email",
me = async (r, e) => {
let o = oe.default.identity.getRedirectURL(),
a = `https://accounts.google.com/o/oauth2/auth?client_id=${jn}&access_type=offline&response_type=code&redirect_uri=${encodeURIComponent(o)}&scope=${encodeURIComponent(Dn)}&prompt=consent`,
n = await oe.default.identity.launchWebAuthFlow({
interactive: r,
url: a
});
if (n) {
let d = new URL(n).searchParams.get("code"),
s = await (await fetch("https://www.youtubedigest.app/api/exchange", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
authCode: d
})
})).json(),
p = s.accessToken,
i = s.refreshToken;
if (p) {
let l = p,
T = await (await fetch(`https://www.googleapis.com/oauth2/v2/userinfo?access_token=${l}`)).json(),
w = T.email;
await oe.default.storage.local.set({
gAuth: {
gAccessToken: l,
gRefreshToken: i,
userEmail: w
}
}), e && e(l, T)
} else console.log("Error signing in: access token not found in response URL", n, s)
} else console.log("Error signing in: user did not grant permission")
}Twitter OAuth code uses the same exchange endpoint
Wt = async (r, e) => {
let o = "ZkwzTm5HV0pFVG15cElDR0c3eWc6MTpjaQ",
a = de.default.identity.getRedirectURL(),
n = `https://twitter.com/i/oauth2/authorize?response_type=code&client_id=${o}&redirect_uri=${encodeURIComponent(a)}&scope=tweet.write%20users.read%20tweet.read%20offline.access&state=state&code_challenge=challenge&code_challenge_method=plain`,
f = await de.default.identity.launchWebAuthFlow({
interactive: r,
url: n
});
if (f) {
let g = new URL(f).searchParams.get("code"),
p = await (await fetch("https://www.youtubedigest.app/api/exchange", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
authCode: g,
type: "twitter"
})
})).json(),
i = p.accessToken,
l = p.refreshToken;
if (i) {
let x = i,
w = await (await fetch(`https://www.youtubedigest.app/api/twitter/user?accessToken=${i}`)).json();
await de.default.storage.local.set({
twitterAuth: {
twitterAccessToken: x,
twitterRefreshToken: l,
username: w.data.username,
id: w.data.id,
name: w.data.name
}
}), e && e(x, w)
} else console.log("Error signing in: access token not found in response URL", f, p)
} else console.log("Error signing in: user did not grant permission")
}Options-page sign-in button reaches the background handler
bt = ke(async () => {
Le && Le.postMessage({
type: "SIGN_IN"
})
}, [Le])else if (e.type === "SIGN_IN") await ho((o, a) => {
r.postMessage({
type: "SIGNED_IN",
email: a.email
})
});
else if (e.type === "TWITTER_SIGN_IN") await Wt(!0, (o, a) => {
r.postMessage({
type: "TWITTER_SIGNED_IN",
userInfo: a
})
});
else if (e.type === "SILENT_SIGN_IN") {
try {
let o = await U.default.storage.local.get("gAuth");
if (o && o.gAuth) {
let a = o.gAuth.userEmail;
r.postMessage({
type: "SIGNED_IN",
email: a
});
return
}
} catch (o) {
console.log("failed in getting user identity from storage", o)
}
await me(!1, (o, a) => {
r.postMessage({
type: "SIGNED_IN",
email: a.email
})
})
}- www.youtubedigest.app
Receives the OAuth authorization code and returns access and refresh tokens to the extension.
- accounts.google.com
Hosts the Google OAuth authorization page opened by the extension.
- twitter.com
Hosts the Twitter OAuth authorization page opened by the extension.
ChatGPT Session Token Extracted and Used for API Access
YoutubeDigest fetches your ChatGPT session token from chatgpt.com/api/auth/session for summaries. 9 captured GETs returned a short-lived accessToken, cached 10s, used against chatgpt.com/backend-api/conversation.
Not sent to the developer.
You request a video summary using the ChatGPT summarization option while logged into ChatGPT.
The extension reads your active ChatGPT session token from chatgpt.com/api/auth/session without asking for explicit authorization, then uses it to make API calls on your behalf.
Our dynamic analysis captured 9 successful GET requests to this endpoint during a single session (dynamic analysis).
Session token extraction, as shipped in background.js
const TOKEN_CACHE_KEY = 'accessToken';
const tokenCache = new LRUCache(10 * 1000); // 10-second TTL
async function fetchChatGptToken() {
// Return cached token if still valid
if (tokenCache.get(TOKEN_CACHE_KEY)) return tokenCache.get(TOKEN_CACHE_KEY);
// Fetch the authenticated session endpoint — no explicit user consent prompt
const response = await fetch('https://chatgpt.com/api/auth/session');
if (response.status === 403) throw new Error('CLOUDFLARE');
const session = await response.json().catch(() => ({}));
if (!session.accessToken) throw new Error('UNAUTHORIZED');
// Cache and return the JWT
tokenCache.set(TOKEN_CACHE_KEY, session.accessToken);
return session.accessToken;
}| Cookie | __Secure-next-auth.session-token=<session-cookie> |
- chatgpt.com
Session token is fetched from /api/auth/session and then used as a Bearer token in POST requests to /backend-api/conversation containing the YouTube video transcript.
What it can do
Permissions this extension asks for, as declared in version 2.4.1.1. 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 openai.com
https://*.openai.com/
Sign you in with your Google account
identity
Store data in your browser
storage