Command Injection in Nestjs with Mutual Tls
Command Injection in Nestjs with Mutual Tls — how this specific combination creates or exposes the vulnerability
Command Injection occurs when an attacker can inject and execute arbitrary system commands through an application. In a NestJS application, this commonly arises when user-controlled input is passed to Node.js functions like child_process.exec, child_process.spawn, or eval. Mutual Transport Layer Security (mTLS) itself does not introduce command injection, but it changes the trust boundary and data sources that developers consider safe.
With mTLS, the server authenticates the client using a client certificate, and the client authenticates the server using the server’s certificate. This mutual authentication can lead to a false sense of security: developers may assume that because the TLS channel is authenticated, inputs are trustworthy. However, mTLS only guarantees that the connected peer has a valid certificate; it does not guarantee that the peer’s application logic is secure or that the data it sends is safe. An authenticated client can still supply malicious input.
In NestJS, if you use mTLS for transport-layer authentication and then pass data from the authenticated client (e.g., from headers, query parameters, or the request body) directly to a system command, you create a command injection path. For example, an endpoint that accepts a filename header to generate a report and then runs exec to call an external tool like pdftotext becomes vulnerable if the header is not strictly validated. An attacker with a valid client certificate can supply a payload such as report.pdf; cat /etc/passwd, leading to command injection despite mTLS being in place.
Additionally, mTLS can complicate logging and monitoring because encrypted traffic is authenticated but not decrypted at the application layer. Security controls that inspect payloads for command injection patterns may miss injected commands if they rely on unencrypted inspection points. The combination of mTLS and insufficient input validation around system command construction creates a scenario where trusted-channel assumptions override secure coding practices.
Consider a NestJS service that invokes an external utility:
import { exec } from 'child_process';
import { Injectable } from '@nestjs/common';
@Injectable()
export class ReportService {
generate(filename: string): string {
// Dangerous: user input used directly in command
const cmd = `pdftotext ${filename} -`;
return exec(cmd);
}
}
If filename originates from an mTLS-authenticated request without strict validation (e.g., allowing '; cat /etc/passwd), the system command executes unintended operations. mTLS ensures the request comes from a certificate holder, but it does not sanitize the filename.
The OWASP API Security Top 10 category API1:2023 – Broken Object Level Authorization and API5:2023 – Broken Function Level Authorization are relevant here because command injection often results from overly permissive authorization and insufficient input validation. Even with mTLS, you must apply strict allowlists, avoid interpolating inputs into shell commands, and use parameterized process execution.
Mutual Tls-Specific Remediation in Nestjs — concrete code fixes
Remediation focuses on strict input validation, avoiding shell interpolation, and leveraging safe execution patterns. Below are concrete, working examples for a NestJS application configured for mTLS.
1. Configure mTLS in NestJS (platform-level)
Ensure your NestJS application enforces client certificate verification. This example uses an Express adapter with an HTTPS server:
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 server = https.createServer({
cert: fs.readFileSync('path/to/server-cert.pem'),
key: fs.readFileSync('path/to/server-key.pem'),
ca: fs.readFileSync('path/to/ca-cert.pem'),
requestCert: true,
rejectUnauthorized: true, // enforce client cert verification
});
app.setGlobalPrefix('api');
await app.init();
server.listen(3000, () => console.log('mTLS server listening on 3000'));
}
bootstrap();
This configuration ensures that only clients presenting a certificate signed by the trusted CA can establish a TLS connection. Note: mTLS is enforced at the transport layer; application logic must still validate data.
2. Avoid shell command construction with user input
Never concatenate user input into shell commands. Use argument arrays with child_process.spawn to avoid injection:
import { spawn } from 'child_process';
import { Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ReportService {
generate(filename: string): Promise {
// Validate filename strictly: allow only alphanumeric, dash, underscore, and .pdf
if (!/^[a-zA-Z0-9_.-]+\.pdf$/.test(filename)) {
throw new BadRequestException('Invalid filename');
}
return new Promise((resolve, reject) => {
const child = spawn('pdftotext', [filename, '-']);
let output = '';
child.stdout.on('data', (data) => (output += data));
child.stderr.on('data', (data) => reject(new Error(data.toString())));
child.on('close', (code) => {
if (code !== 0) reject(new Error(`pdftotext exited with code ${code}`));
resolve(output);
});
});
}
}
3. Validate and sanitize all inputs from mTLS-authenticated requests
Treat data from authenticated clients as untrusted. Use class-validator in NestJS to enforce strict schemas:
import { IsString, Matches } from 'class-validator';
export class GenerateReportDto {
@Matches(/^[a-zA-Z0-9_.-]+\.pdf$/, {
message: 'Filename contains invalid characters',
})
filename: string;
}
// In controller
import { Body, Controller, Post } from '@nestjs/common';
import { ValidatePipe } from '@nestjs/common';
@Controller('report')
export class ReportController {
@Post('generate')
generate(@Body(new ValidatePipe()) dto: GenerateReportDto) {
return this.reportService.generate(dto.filename);
}
}
4. Principle of least privilege for external tools
Ensure the process running the NestJS application has minimal permissions. Do not run the application as root. Restrict what external commands can be executed and from which directories. This limits the impact if a command injection flaw is present despite validation.
By combining mTLS transport security with rigorous input validation, safe process execution, and least-privilege execution contexts, you mitigate command injection risks while retaining the benefits of mutual authentication.
Related CWEs: inputValidation
| CWE ID | Name | Severity |
|---|---|---|
| CWE-20 | Improper Input Validation | HIGH |
| CWE-22 | Path Traversal | HIGH |
| CWE-74 | Injection | CRITICAL |
| CWE-77 | Command Injection | CRITICAL |
| CWE-78 | OS Command Injection | CRITICAL |
| CWE-79 | Cross-site Scripting (XSS) | HIGH |
| CWE-89 | SQL Injection | CRITICAL |
| CWE-90 | LDAP Injection | HIGH |
| CWE-91 | XML Injection | HIGH |
| CWE-94 | Code Injection | CRITICAL |