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:
- Logging tokens —
captureError()would send the full message to the error-reporting pipeline, potentially persisting token values in log storage. - Leaking tokens to API callers — tRPC error handling could surface the raw message as a
BAD_GATEWAYresponse 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:
| Pattern | Replaced 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:
- Calls
redactSensitiveData()on the raw HTTP response body. - Truncates the sanitised result to 500 characters to prevent large error payloads from bloating error reports.
- Constructs the message as
TrueLayer <operation> failed (<status>): <sanitised body>. - Exposes
status: numberandoperation: stringas 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
TruelayerApiExceptioncorrectly storesname,status, andoperation- Body truncation at 500 characters works as expected
instanceof Errorreturnstrue(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 toerr.statusis recommended. - Exception type —
TruelayerApiExceptionextendsError, socatch (err)blocks that do not check the type continue to work. Code that previously checkederr instanceof Errorwill 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.