Crlf Injection in Nestjs with Mutual Tls
Crlf Injection in Nestjs with Mutual Tls — how this specific combination creates or exposes the vulnerability
Crlf Injection occurs when an attacker can inject carriage return (CR, \r) and line feed (\n) characters into a header or status line, causing the server to prematurely terminate a line and inject additional headers or split responses. In NestJS, this often arises when user-controlled input is concatenated into HTTP response headers without proper sanitization, for example when reflecting a username or a redirect URL directly into res.set() or res.header() calls.
When Mutual TLS (mTLS) is enforced, the server validates the client certificate before application code runs. This validation can create a false sense of security: operators assume that only trusted clients can interact with the API, so they relax input validation. However, mTLS does not sanitize or validate the content of requests; it only confirms identity. Therefore, an authenticated mTLS client—such as a compromised microservice or a malicious insider with a valid certificate—can still supply malicious header values. Because NestJS applications frequently build dynamic headers (e.g., X-User-ID, Location, Set-Cookie) from request data, the presence of mTLS can lead developers to skip normalization and encoding, inadvertently enabling header splitting via CRLF sequences like %0D%0A or raw \r\n.
The combination of mTLS and permissive header handling also interacts with logging and observability. Access logs and traces may include the reflected header values, allowing injected CRLF sequences to corrupt log formatting and potentially facilitate log injection attacks. In distributed setups behind an mTLS-terminating proxy, if the proxy passes through headers unchecked and the NestJS app trusts the proxied values, an attacker with a valid client certificate can inject newline characters that split the response stream, enabling response smuggling or cache poisoning.
For example, consider a NestJS endpoint that echoes a user-supplied label into a custom response header:
const label = req.query.label || 'default';
res.set('X-Custom-Label', label);
res.json({ ok: true });
If the query parameter label contains example\r\nSet-Cookie: session=attacker, the response becomes:
HTTP/1.1 200 OK
X-Custom-Label: example
Set-Cookie: session=attacker
Content-Type: application/json; charset=utf-8
{"ok":true}
Even with mTLS ensuring only authorized clients connect, this injection can manipulate session handling or hide malicious headers. The OWASP API Top 10 categorizes this as an injection issue, and mappings to PCI-DSS and SOC2 highlight the risk to data integrity and monitoring integrity.
Mutual Tls-Specific Remediation in Nestjs — concrete code fixes
Remediation focuses on strict input validation, output encoding, and architectural safeguards, irrespective of mTLS. Treat authenticated mTLS clients as untrusted for data content, and apply the same header-safety practices as for any public API.
- Validate and sanitize header inputs: Never reflect raw user input into response headers. Use allowlists and strict patterns. For strings that must appear in headers, remove or encode CR and LF characters.
import { filter, replace } from 'lodash';
function sanitizeHeaderValue(value: string): string {
// Remove CR and LF to prevent header splitting
return replace(value, /[\r\n]+/g, '');
}
const rawLabel = req.query.label || 'default';
const safeLabel = sanitizeHeaderValue(rawLabel);
res.set('X-Custom-Label', safeLabel);
res.json({ ok: true });
- Use NestJS interceptors or middleware for centralized header handling: This ensures consistent validation across all routes and prevents accidental omissions in new endpoints.
@Injectable()
export class HeaderSanitizationInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const ctx = context.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
// Example: sanitize a known header reflected from query params
if (request.query?.label) {
const safeLabel = request.query.label.replace(/[\r\n]/g, '');
response.set('X-Custom-Label', safeLabel);
}
return next.handle();
}
}
@Controller()
export class AppController {
constructor() {}
@UseInterceptors(HeaderSanitizationInterceptor)
@Get('echo')
echo() {
return { ok: true };
}
}
- Explicitly set headers instead of relying on framework defaults: Avoid methods that implicitly append headers with user-controlled values. Prefer
res.set()with sanitized values over chaining untrusted input directly.
- Enforce mTLS at the proxy or gateway and keep application-layer validation independent: Do not rely on the TLS layer to validate header content. Use the NestJS security middleware to enforce Content Security Policy and ensure response headers are hardened against splitting.
// Example of strict header policy in main.ts
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', "default-src 'self'");
next();
});
- Leverage OpenAPI/Swagger schema validation to reject unsafe inputs: Define string patterns that exclude control characters. With middleBrick, you can scan your OpenAPI spec and runtime behavior to detect endpoints that reflect headers without validation, ensuring alignment with OWASP API Top 10 and compliance mappings such as PCI-DSS and SOC2.
These measures ensure that even in an mTLS-protected environment, your NestJS application remains resilient against CRLF Injection, preserving log integrity and preventing response smuggling or cache poisoning.