All Docs
FeaturesMaking Tax DigitalUpdated February 24, 2026

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:

  1. 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.
  2. In the callback — read the state value 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:

  1. Initiate their own TrueLayer flow to obtain a valid authorisation code for their own bank account.
  2. Construct a callback URL containing that code and a victim's orgId as the state.
  3. 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 orgId are stored server-side in a signed HttpOnly cookie, inaccessible to JavaScript.
  • State verification — the callback rejects any request where the returned state does not match the stored nonce.
  • orgId removed from bare stateorgId is now carried inside the signed cookie payload, not exposed as the raw state string.

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.

References