HIGH command injectionnestjsmutual tls

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 IDNameSeverity
CWE-20Improper Input Validation HIGH
CWE-22Path Traversal HIGH
CWE-74Injection CRITICAL
CWE-77Command Injection CRITICAL
CWE-78OS Command Injection CRITICAL
CWE-79Cross-site Scripting (XSS) HIGH
CWE-89SQL Injection CRITICAL
CWE-90LDAP Injection HIGH
CWE-91XML Injection HIGH
CWE-94Code Injection CRITICAL

Frequently Asked Questions

Does mTLS prevent command injection in NestJS applications?
No. Mutual TLS authenticates the client and server at the transport layer, but it does not validate or sanitize application-level inputs. If user-supplied data is passed to system commands without strict validation and safe execution patterns, command injection remains possible even with mTLS enabled.
What is a safe way to handle filenames in a NestJS API secured with mTLS?
Treat filenames as untrusted input regardless of mTLS. Use strict allowlist validation (e.g., regex ^[a-zA-Z0-9_.-]+\.pdf$), avoid shell interpolation by using argument arrays with child_process.spawn, and apply the principle of least privilege to the runtime environment.