Auth Bypass in Nestjs with Mutual Tls
Auth Bypass in Nestjs with Mutual Tls — how this specific combination creates or exposes the vulnerability
Mutual Transport Layer Security (mTLS) requires both the client and the server to present valid certificates during the TLS handshake. In NestJS, developers often enable mTLS to strengthen authentication, assuming that a client certificate alone is sufficient to authorize requests. This assumption can lead to an authentication bypass when mTLS is enforced at the transport layer but authorization logic is missing or incomplete at the application layer.
For example, a NestJS app might verify the client certificate presence and basic validity (e.g., not expired, trusted issuer) but then rely solely on the certificate’s subject or serial number to identify the user or role. If the application does not cross-check this identity against a permissions model (such as roles or scopes stored in a database), an attacker who obtains a valid client certificate can access endpoints they should not be allowed to reach. This is a Broken Level of Authorization (BOLA/IDOR) pattern specific to mTLS deployments.
Additionally, misconfigured middleware or guards can inadvertently skip authorization when mTLS client details are available in the request object. In NestJS, the client certificate is typically exposed via req.socket.getPeerCertificate(). If a guard reads the certificate but fails to enforce role-based checks for certain routes, the effective authentication does not imply proper authorization. An unauthenticated attacker scenario is less likely with mTLS since the handshake rejects unknown clients, but a compromised or improperly issued certificate can lead to unauthorized access across endpoints that do not validate scope or tenant boundaries.
Another subtle risk arises when developers inspect the certificate only in an entry point (such as an interceptor) and assume it applies uniformly across all downstream controllers and services. If some routes rely on this early inspection while others repeat the check with different logic, inconsistent enforcement can allow an authenticated client to perform actions outside their intended permissions. This inconsistency is detectable through systematic endpoint analysis that compares required authentication contexts with actual authorization checks, a capability included in continuous monitoring approaches like those offered by the Pro plan for cross-referencing runtime behavior with OpenAPI specifications.
Real-world attack patterns include scenarios where a client certificate is issued for one service or tenant but reused or forwarded to endpoints intended for another service or tenant. Without proper property-level or tenant-level authorization, such requests can read or modify data belonging to others. These findings map to the OWASP API Top 10 category for Broken Object Level Authorization and can be reflected in compliance frameworks such as SOC2 and GDPR when access controls are not rigorously enforced.
Mutual Tls-Specific Remediation in Nestjs — concrete code fixes
To mitigate authentication bypass risks in NestJS with mTLS, combine strict transport configuration with explicit application-level authorization checks. Below are concrete code examples that show how to enforce mTLS and validate client identity before allowing access to protected routes.
1. Enabling mTLS in NestJS with an HTTPS server
Configure the NestJS application to require client certificates by setting up an HTTPS server with requestCert and rejectUnauthorized options. This ensures that only clients with a trusted certificate can complete the handshake.
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as fs from 'fs';
import * as https from 'https';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const httpsOptions = {
key: fs.readFileSync('path/to/server.key'),
cert: fs.readFileSync('path/to/server.crt'),
ca: fs.readFileSync('path/to/ca.crt'),
requestCert: true,
rejectUnauthorized: true,
};
const httpsServer = https.createServer(httpsOptions, app.getHttpAdapter().getInstance());
await app.init();
app.getHttpAdapter().getInstance().server = httpsServer;
}
bootstrap();
2. Extracting and validating the client certificate in a guard
Create an authorization guard that reads the client certificate and enforces role- or scope-based checks. This ensures that a valid certificate maps to an allowed identity and permission set.
// src/auth/mtls-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import * as tls from 'tls';
@Injectable()
export class MtlsAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const cert = request.socket.getPeerCertificate();
// Reject if no certificate presented (should not happen with rejectUnauthorized=true)
if (!cert || Object.keys(cert).length === 0) {
return false;
}
// Example: enforce specific extended key usage or SAN patterns
const allowedIssuer = 'CN=Trusted CA,O=Example Org';
const allowedRoles = ['api-client-service-a', 'api-client-service-b'];
if (cert.issuerName !== allowedIssuer) {
return false;
}
// Extract role from certificate subject or custom extension
const subject = cert.subject;
const roleMatch = subject.match(/role=([^,]+)/);
const role = roleMatch ? roleMatch[1] : null;
if (!role || !allowedRoles.includes(role)) {
return false;
}
// Attach identity to request for downstream use
request.user = {
subject: subject,
role: role,
fingerprint: cert.fingerprint,
};
return true;
}
}
3. Applying the guard to routes and combining with property-level checks
Use the guard on controllers and, where needed, add property-level authorization to ensure tenants or resource ownership are validated for each request.
// src/users/users.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { MtlsAuthGuard } from '../auth/mtls-auth.guard';
import { UserService } from './user.service';
@Controller('users')
@UseGuards(MtlsAuthGuard)
export class UsersController {
constructor(private readonly userService: UserService) {}
@Get(':id')
getUserById(@Param('id') id: string, @Req() req: any) {
// Ensure the authenticated subject can access this resource
const requester = req.user;
return this.userService.getUserById(id, requester.role, requester.subject);
}
}
4. Validating certificate metadata against an allowlist
For higher assurance, compare certificate fingerprints or serial numbers against an allowlist stored securely in the application. This prevents misuse of revoked or out-of-scope certificates even if they chain to a trusted CA.
// src/auth/mtls-allowlist.service.ts
import { Injectable } from '@nestjs/common';
import * as fs from 'fs';
@Injectable()
export class MtlsAllowlistService {
private allowedFingerprints = new Set();
constructor() {
const data = fs.readFileSync('path/to/allowlist.json', 'utf-8');
const entries = JSON.parse(data);
entries.forEach((entry: { fingerprint: string }) => this.allowedFingerprints.add(entry.fingerprint));
}
isAllowed(fingerprint: string): boolean {
return this.allowedFingerprints.has(fingerprint);
}
}
By enforcing mTLS at the transport layer, validating certificate metadata explicitly in guards, and applying property-level authorization, you reduce the risk of authentication bypass in NestJS applications. These practices complement continuous scanning strategies that compare runtime behavior against specifications and help detect inconsistencies before they can be exploited.
Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |