Summary
The GET /api/connect/github/callback route in apps/backend/src/routes/connect.ts has no authentication guard. The userId used to store the connected GitHub token is read directly from a base64-encoded URL state parameter that the attacker controls entirely.
Attack scenario
- Attacker learns or guesses a victim's internal
userId (e.g. from any public API response that returns user data).
- Attacker completes a GitHub OAuth flow using their own account, intercepting the
code before it is submitted to the server.
- Attacker crafts a
state payload: base64({"userId":"<victim-uuid>","nonce":"anything"}).
- Attacker POSTs
GET /api/connect/github/callback?code=<their_code>&state=<forged_state>.
- Server stores the attacker's GitHub token under the victim's account.
- The follow button on the victim's DevCard now triggers GitHub follows using the attacker's credentials, bypassing the victim's consent.
Root cause
// connect.ts line 55 -- no preHandler:
app.get('/github/callback', async (request, reply) => {
...
const userId = decodedState.userId; // attacker-supplied, never verified against session
await app.prisma.oAuthToken.upsert({ ... userId ... });
The /github initiation route and all other connect routes correctly require preHandler: [app.authenticate]. The callback is the only exception.
Related issue: weak OAuth state entropy
Both auth.ts (line 322) and connect.ts (line 173) use Math.random().toString(36) to generate OAuth state tokens. Math.random is not a CSPRNG and provides only ~42 bits of entropy, which is insufficient for security-sensitive nonces.
function generateState(): string {
return Math.random().toString(36).substring(2, 15); // not CSPRNG
}
Fix
- Add
preHandler: [app.authenticate] to the callback route.
- After decoding state, verify
decodedState.userId === request.user.id before proceeding.
- Replace
Math.random() with crypto.randomBytes(32).toString('hex') in both files.
/assign gssoc
Summary
The
GET /api/connect/github/callbackroute inapps/backend/src/routes/connect.tshas no authentication guard. TheuserIdused to store the connected GitHub token is read directly from a base64-encoded URLstateparameter that the attacker controls entirely.Attack scenario
userId(e.g. from any public API response that returns user data).codebefore it is submitted to the server.statepayload:base64({"userId":"<victim-uuid>","nonce":"anything"}).GET /api/connect/github/callback?code=<their_code>&state=<forged_state>.Root cause
The
/githubinitiation route and all other connect routes correctly requirepreHandler: [app.authenticate]. The callback is the only exception.Related issue: weak OAuth state entropy
Both
auth.ts(line 322) andconnect.ts(line 173) useMath.random().toString(36)to generate OAuth state tokens.Math.randomis not a CSPRNG and provides only ~42 bits of entropy, which is insufficient for security-sensitive nonces.Fix
preHandler: [app.authenticate]to the callback route.decodedState.userId === request.user.idbefore proceeding.Math.random()withcrypto.randomBytes(32).toString('hex')in both files./assign gssoc