Security Advisory: Bank OAuth Callback CSRF Fix (v1.0.37)
Security Advisory: Bank OAuth Callback CSRF State Validation Fix
Version: 1.0.37
Severity: High
Affected component: TrueLayer bank account connection (src/app/api/bank/callback/route.ts)
Status: ✅ Resolved
Overview
Version 1.0.37 patches a Cross-Site Request Forgery (CSRF) vulnerability in the bank OAuth callback flow. The state parameter used during the TrueLayer bank authorisation redirect was set to a plain orgId value. Because no server-side nonce was generated or verified, an attacker could craft a malicious callback URL that linked an arbitrary bank account to a victim's organisation.
Technical Detail
How OAuth state is supposed to work
The OAuth 2.0 state parameter exists specifically to prevent CSRF attacks. The correct pattern is:
- Before redirect — generate a cryptographically random nonce. Store it (along with any application-specific data such as
orgId) in a signed HttpOnly cookie on the server. - In the callback — read the
statevalue returned by the authorisation server and compare it to the value stored in the cookie. Reject the request if they do not match.
What the bank callback was doing
// ⚠️ VULNERABLE pattern (pre-v1.0.37)
// state was set to the bare orgId string:
const redirectUrl = `https://auth.truelayer.com/?...&state=${orgId}`;
// In the callback, orgId was read straight from state with no validation:
const { orgId } = parse(state);
await linkBankAccount(orgId, authCode);
Because no cookie was set and no comparison was performed, an attacker could:
- Initiate their own TrueLayer flow to obtain a valid authorisation code for their own bank account.
- Construct a callback URL containing that code and a victim's
orgIdas thestate. - Trick the victim (or replay the request directly) into triggering the callback, linking the attacker's bank account to the victim's organisation.
What the HMRC OAuth callback was already doing correctly
The HMRC OAuth flow already followed the secure pattern — a signed nonce was stored in an HttpOnly cookie before the redirect and verified on return. The bank callback was not consistent with this implementation.
Resolution
The bank OAuth callback now mirrors the HMRC OAuth flow:
// ✅ SECURE pattern (v1.0.37+)
// 1. Before redirecting to TrueLayer:
const nonce = crypto.randomBytes(32).toString('hex');
const statePayload = sign({ nonce, orgId }); // signed/encoded
setCookie('bank_oauth_state', statePayload, { httpOnly: true, sameSite: 'lax' });
const redirectUrl = `https://auth.truelayer.com/?...&state=${nonce}`;
// 2. In the callback:
const storedPayload = getCookie('bank_oauth_state');
const { nonce: storedNonce, orgId } = verify(storedPayload);
if (state !== storedNonce) {
return new Response('Invalid state', { status: 403 });
}
await linkBankAccount(orgId, authCode);
Key changes:
- Random nonce — a cryptographically random value is generated per-request before the redirect.
- Signed HttpOnly cookie — the nonce and
orgIdare stored server-side in a signed HttpOnly cookie, inaccessible to JavaScript. - State verification — the callback rejects any request where the returned
statedoes not match the stored nonce. orgIdremoved from bare state —orgIdis now carried inside the signed cookie payload, not exposed as the rawstatestring.
Who Is Affected?
All installations running a version prior to v1.0.37 that have the TrueLayer bank connection feature enabled. Upgrade immediately.
Recommended Action
- Upgrade to v1.0.37 or later as soon as possible.
- After upgrading, review any recently linked bank accounts for unexpected connections.
- If you suspect unauthorised bank account linking occurred, unlink all bank accounts and re-authorise through the updated flow.