Bleichenbacher Attack in Nestjs with Bearer Tokens
Bleichenbacher Attack in Nestjs with Bearer Tokens — how this specific combination creates or exposes the vulnerability
A Bleichenbacher attack is a chosen-ciphertext attack against asymmetric encryption padding schemes, most commonly PKCS#1 v1.5 in RSA. In a NestJS API that uses Bearer Tokens for authentication, this attack can manifest when an API endpoint performs decryption or signature verification using a private key or secret and reveals distinct timing differences or error messages depending on whether the padding is valid. An attacker who can send modified Bearer Tokens (e.g., JWTs or custom bearer payloads) crafted to target the padding verification routine can iteratively learn the plaintext or the key material by observing server responses and elapsed time.
In NestJS, this often occurs in scenarios where developers implement custom JWT verification or encrypt/decrypt tokens using Node.js crypto with RSA and PKCS#1 v1.5 padding. For example, consider an endpoint that decodes a Bearer Token and performs asymmetric decryption to extract user claims:
import { Controller, Get, Req } from '@nestjs/common';import * as crypto from 'crypto';@Controller()export class AuthController { @Get('/user') async getUser(@Req() req: any) { const auth = req.headers['authorization']; if (!auth?.startsWith('Bearer ')) { throw new Error('Unauthorized'); } const token = auth.split(' ')[1]; // Vulnerable: using crypto.privateDecrypt with PKCS#1 v1.5 padding const decrypted = crypto.privateDecrypt( { key: process.env.PRIVATE_KEY_PKCS1, padding: crypto.constants.RSA_PKCS1_PADDING }, Buffer.from(token, 'base64') ); const payload = JSON.parse(decrypted.toString('utf8')); return { userId: payload.sub }; }}If the server returns different error responses such as BadPaddingError versus other errors, or if the decryption/verification time varies measurably, an attacker can exploit this as a side channel. By sending many Bearer Tokens where only the padding is altered, and measuring success or timing, the attacker gradually decrypts an intercepted token without needing the private key. This is a server-side issue enabled by the use of weak padding and non-constant-time verification, and it is especially dangerous when Bearer Tokens are treated as opaque but are decrypted server-side in a non-hardened way.
Even when using JWT libraries, a NestJS service that manually verifies RSA signatures with PKCS#1 v1.5 and does not enforce strict padding validation or constant-time comparison can be vulnerable. For instance:
import { Injectable } from '@nestjs/common';import * as crypto from 'crypto';import { createVerify } from 'crypto';@Injectable()export class JwtValidatorService { verifyToken(token: string): boolean { const [headerB64, payloadB64, signatureB64] = token.split('.'); const data = `${headerB64}.${payloadB64}`; const verifier = createVerify('RSA-SHA256'); verifier.update(data); // Vulnerable: uses legacy verify with PKCS#1 padding by default in some Node versions return verifier.verify(process.env.PUBLIC_KEY_PEM, Buffer.from(signatureB64 + '==', 'base64')); }}Here, if the underlying verify implementation does not enforce strict, constant-time padding validation, an attacker can mount a Bleichenbacher-style adaptive attack by observing whether the server accepts or rejects manipulated signatures. The combination of Bearer Tokens (which may carry encrypted or signed claims) with server-side decryption/verification that leaks padding errors creates an exploitable condition in NestJS applications that do not enforce modern, safe padding (OAEP) or use constant-time verification patterns.
Bearer Tokens-Specific Remediation in Nestjs — concrete code fixes
Remediation focuses on avoiding legacy padding schemes and ensuring constant-time verification. For RSA encryption, prefer RSA-OAEP instead of PKCS#1 v1.5. For signatures, prefer RSASSA-PSS or use maintained libraries that handle constant-time comparison internally. Below are concrete, secure examples for NestJS.
Secure RSA-OAEP decryption
Replace PKCS#1 v1.5 with OAEP, which is not malleable and does not leak padding information:
import { Controller, Get, Req } from '@nestjs/common';import * as crypto from 'crypto';@Controller()export class AuthController { @Get('/user') async getUser(@Req() req: any) { const auth = req.headers['authorization']; if (!auth?.startsWith('Bearer ')) { throw new Error('Unauthorized'); } const token = auth.split(' ')[1]; try { const decrypted = crypto.privateDecrypt( { key: process.env.PRIVATE_KEY_PEM, oaepHash: 'sha256', label: undefined, }, Buffer.from(token, 'base64') ); const payload = JSON.parse(decrypted.toString('utf8')); return { userId: payload.sub }; } catch (err) { // Use a generic error to avoid leaking padding or decryption details throw new Error('Invalid token'); } }}Secure signature verification with PSS and constant-time handling
Use PSS padding for RSA signatures and ensure errors are handled uniformly:
import { Injectable } from '@nestjs/common';import * as crypto from 'crypto';@Injectable()export class JwtValidatorService { verifyToken(token: string): boolean { try { const [headerB64, payloadB64, signatureB64] = token.split('.'); const data = `${headerB64}.${payloadB64}`; const verifier = crypto.createVerify('RSA-SHA256'); verifier.update(data); // Use PSS key with salt length -1 for maximum security const publicKey = `-----BEGIN PUBLIC KEY-----\n${process.env.PUBLIC_KEY_PEM}\n-----END PUBLIC KEY-----`; return verifier.verify(publicKey, Buffer.from(signatureB64 + '==', 'base64'), 32 /* pss salt length */); } catch { return false; // constant-time failure path } }}Additionally, consider using a maintained JWT library (e.g., jsonwebtoken with modern algorithms like RS256 and RSASSA-PSS) and ensure that error handling does not distinguish between malformed tokens and invalid signatures. For Bearer Tokens that carry encrypted data, validate using authenticated encryption (e.g., RSA-OAEP or AES-GCM) so that decryption either fully succeeds or fails with a generic error, eliminating timing and padding side channels.
Finally, rotate keys and enforce short token lifetimes to reduce the impact of any successful attack. With these changes, the NestJS application mitigates the conditions required for a Bleichenbacher-style adaptive chosen-ciphertext attack on Bearer Tokens.