Missing OAuth state Parameter Enables CSRF-Driven Account Takeover in a JavaScript SDK
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.