Critical severityWeb

Missing OAuth state Parameter Enables CSRF-Driven Account Takeover in a JavaScript SDK

Afaq AmjadFebruary 24, 20266 min read

Type: CWE-352 CSRF in OAuth · Severity: Critical · Reported to a private bug-bounty program via HackerOne.

Vendor and package names are redacted per coordinated-disclosure rules; this covers the vulnerability classes only.

1. Critical — OAuth flow has no CSRF state

The SDK's OAuth helper set the state parameter to undefined when launching the authorization flow, so no CSRF token was generated or validated:

const request = new AuthorizationRequest({
  client_id: this.clientId,
  redirect_uri: redirectUri,
  scope: scope,
  response_type: AuthorizationRequest.RESPONSE_TYPE_CODE,
  state: undefined,   // <-- no CSRF protection
}, undefined, true);

Without state, an attacker can craft an authorization link that binds their authorization to the victim's session — the classic OAuth CSRF leading to account/wallet takeover.

Fix: generate a cryptographically random state, persist it (e.g. sessionStorage), and verify it on the callback:

const state = crypto.randomUUID();
sessionStorage.setItem("oauth_state", state);
// ...on callback:
if (response.state !== sessionStorage.getItem("oauth_state"))
  throw new Error("OAuth state mismatch - possible CSRF");

2. High — authorization codes logged to the console

The SDK logged the OAuth authorization code to the browser console:

console.log(`Authorization Code ${response.code}`);

That exposes the auth code in dev tools, to browser extensions, and in error reporters — enabling session hijacking. Strip all sensitive console.log calls from production builds.

3. Medium — fail-open authentication

try {
  const accessToken = await this.oauthHelper.getFreshAccessToken();
  return { ...params, authorization: `Bearer ${accessToken}` };
} catch (e) {
  return params; // proceeds unauthenticated!
}

When token refresh fails the SDK silently sends the request without auth (CWE-636). Fail closed: throw instead of downgrading.

Credit where due

The SDK did several things right — timingSafeEqual for webhook signatures, RSA-PSS 4096/SHA-256, PBKDF2, AES-GCM, and crypto.getRandomValues(). The lesson is that one state: undefined undoes a lot of otherwise solid crypto hygiene.

#OAuth#CSRF#CWE-352#Fail-Open#SDK Security

More research

Think your systems could have this?

Let Faseel find it before an attacker does.

Request an Assessment