Skip to main content
All Docs
FeaturesMaking Tax DigitalUpdated March 26, 2026

Security Fix: TrueLayer Error Body Sanitisation (SCR-12)

Security Fix: TrueLayer Error Body Sanitisation (SCR-12)

Released in: v1.0.463
Severity: High
Component: TrueLayer bank-feed client

Background

The Making Tax Digital platform connects to HMRC-recognised bank feeds via TrueLayer to import transaction data for landlords and self-employed taxpayers. This connection involves a standard OAuth 2.0 flow in which short-lived access tokens and long-lived refresh tokens are exchanged between the platform and TrueLayer's API.

What Was the Risk

When any of the five TrueLayer client methods returned a non-2xx HTTP status, the platform caught the response body and immediately embedded it in a plain Error message:

TrueLayer exchangeCode failed (400): {"error":"invalid_grant","access_token":"eyJ...","refresh_token":"4b6f..."}

TrueLayer OAuth error responses sometimes include sensitive credential-shaped fields (access_token, refresh_token, client_secret) in the response body. By forwarding this text verbatim, the platform was:

  1. Logging tokenscaptureError() would send the full message to the error-reporting pipeline, potentially persisting token values in log storage.
  2. Leaking tokens to API callers — tRPC error handling could surface the raw message as a BAD_GATEWAY response body, visible to browser clients.

The five affected methods were: exchangeCode, refreshAccessToken, listAccounts, getBalance, listTransactions.

What Was Fixed

redactSensitiveData(input: string): string

A new utility function applies a chain of regex patterns to any string before it enters an error message. It targets the specific shapes that OAuth secrets take:

PatternReplaced with
"access_token":"..." (and refresh_token, client_secret, id_token, token)"access_token":"[REDACTED]"
Bearer <token>Bearer [REDACTED]
Full JWT (three base64url segments)[REDACTED_JWT]
Hex string ≥ 32 characters[REDACTED_HEX]
Base64 string ≥ 40 characters[REDACTED_B64]

The function deliberately preserves short, human-readable error details such as "error":"invalid_grant" or "error_description":"The code has expired" so that error messages remain actionable for debugging.

// Example — benign details preserved, secrets stripped
const raw = '{"error":"invalid_grant","access_token":"eyJ...long..."}';
redactSensitiveData(raw);
// → '{"error":"invalid_grant","access_token":"[REDACTED]"}'

TruelayerApiException

A new typed Error subclass replaces all five raw throw new Error(...) calls. Its constructor:

  1. Calls redactSensitiveData() on the raw HTTP response body.
  2. Truncates the sanitised result to 500 characters to prevent large error payloads from bloating error reports.
  3. Constructs the message as TrueLayer <operation> failed (<status>): <sanitised body>.
  4. Exposes status: number and operation: string as typed read-only properties so callers can branch on HTTP status without parsing the message string.
try {
  await exchangeCode(code);
} catch (err) {
  if (err instanceof TruelayerApiException) {
    console.log(err.status);    // e.g. 400
    console.log(err.operation); // "exchangeCode"
    // err.message is already sanitised — safe to log
  }
}

Test Coverage

17 unit tests were added in tests/lib/truelayer/client-sanitisation.test.ts. They verify:

  • Each sensitive field type is redacted correctly
  • Bearer tokens and JWTs are stripped
  • Long hex secrets are stripped
  • Benign error codes and descriptions are preserved unchanged
  • Empty strings and short strings pass through unmodified
  • Multiple sensitive values in one body are all redacted
  • TruelayerApiException correctly stores name, status, and operation
  • Body truncation at 500 characters works as expected
  • instanceof Error returns true (prototype chain is correct)

Impact on Existing Integrations

  • Error message format — the message prefix (TrueLayer <method> failed (<status>):) is unchanged. Downstream code that parses the status code out of the message string should still work, though switching to err.status is recommended.
  • Exception typeTruelayerApiException extends Error, so catch (err) blocks that do not check the type continue to work. Code that previously checked err instanceof Error will still match. Code that relied on the raw token values being present in the message will no longer receive them — this is intentional.
  • No changes to public API routes, tRPC procedures, or database schema.