Skip to content

bug: GitHub Connect OAuth callback silently skips CSRF nonce verification when Redis is unavailable — account takeover vector #438

@hariom888

Description

@hariom888

Description:

apps/backend/src/routes/connect.ts stores a one-time nonce in Redis during the /connect/github initiation flow. The callback at /connect/github/callback is supposed to verify this nonce to prevent CSRF attacks. The verification logic is:

const storedUserId = app.redis
  ? await app.redis.get(`oauth:nonce:${decodedState.nonce}`)
  : null;

if (app.redis && (!storedUserId || storedUserId !== decodedState.userId)) {
  // reject
}

Both the read and the guard are conditional on app.redis being truthy. If Redis is unavailable (connection failure, misconfiguration, Redis crash), app.redis is falsy and both branches are skipped entirely. The callback then proceeds to exchange the attacker-supplied code for a token and stores it under the userId decoded from the attacker-controlled state parameter — with zero server-side verification that the state was ever issued by this server.

An attacker who can observe or predict the base64-encoded state format (which is not a secret — it is { userId, nonce } base64-encoded) can construct a valid-looking state for any userId, submit a stolen OAuth code to the callback, and have a github_follow token stored under an arbitrary user's account.

The initiation endpoint (GET /connect/github) unconditionally calls await app.redis.set(...) without a null-guard, meaning if Redis is actually absent it would throw before a nonce is stored — but the callback does not mirror this strictness and degrades silently.

Affected file: apps/backend/src/routes/connect.ts

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

Status
Todo

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions