HIGH command injectionhapimutual tls

Command Injection in Hapi with Mutual Tls

Command Injection in Hapi with Mutual Tls — how this specific combination creates or exposes the vulnerability

Command Injection in a Hapi application using Mutual Transport Layer Security (mTLS) often stems from a mismatch between strong transport authentication and insufficient input validation on the application layer. mTLS ensures that both the client and the server present valid certificates, which strongly authenticates the caller. However, authentication is not authorization, and it does not guarantee that the data sent by an authenticated party is safe to pass to a system-level shell or utility.

In Hapi, routes that accept parameters (path, query, or payload) and forward them directly to operating system utilities—such as through child_process.exec, child_process.spawn, or custom wrappers around CLI tools—can become injection vectors if the input is not strictly validated, sanitized, or constructed using safe patterns. For example, an authenticated client may send a request to /report/{filename} where filename is used in a shell command like cat /data/reports/${filename}. Even with mTLS, a compromised but trusted client, a misconfigured certificate mapping, or a business logic flaw can allow an attacker to inject shell metacharacters (e.g., ;, &, |, &&, backticks, or $()) that lead to arbitrary command execution.

The combination can expose subtle issues: operators may assume mTLS removes the need for input validation because the channel is encrypted and peers are verified. In practice, mTLS does not prevent malicious or malformed data from an authorized client. Hapi route handlers that parse certificates (e.g., extracting a Common Name or organizational unit from request.info.certificate) and then use those values in shell commands amplify risk if the extracted data is used without escaping. Attack patterns such as path traversal via ../ sequences, variable substitution, or command chaining become viable when untrusted input is concatenated into command strings. This highlights that mTLS secures the pipe but does not sanitize the payload; server-side validation, strict allowlists, and avoiding shell invocation remain essential.

Consider a Hapi route that generates a system report by calling a CLI tool with user-supplied arguments:

// Risky example: do not use
const Hapi = require('@hapi/hapi');
const { exec } = require('child_process');

const init = async () => {
  const server = Hapi.server({ port: 4443, tls: { /* mTLS options omitted for brevity */ } });

  server.route({
    method: 'GET',
    path: '/run/{command}',
    options: {
      validate: {
        params: {
          command: Joi.string() // insufficient: allows shell metacharacters
        }
      }
    },
    handler: (request, h) => {
      const userCmd = request.params.command;
      exec(userCmd, (error, stdout, stderr) => { // Command Injection risk
        if (error) return h.response('Error').code(500);
        return h.response(stdout);
      });
    }
  });

  await server.start();
};
init();

Here, mTLS may authenticate the client, but userCmd is passed directly to exec. An authenticated client can supply ls; cat /etc/passwd or other shell sequences, leading to unintended command execution. A safer approach is to avoid the shell entirely, use structured APIs, and apply strict allowlists for any required parameters.

Mutual Tls-Specific Remediation in Hapi — concrete code fixes

Remediation centers on two pillars: (1) strict input validation and safe execution patterns in Hapi route handlers, and (2) correct mTLS configuration on the server to enforce client verification without introducing false confidence. Below are concrete, safe code examples for Hapi.

1. Avoid shell execution; use built-in or language-native APIs

Replace exec and shell-like concatenation with language-native methods. For filesystem operations, use Node.js fs and path normalization instead of shelling out.

const Hapi = require('@hapi/hapi');
const { readFile } = require('fs/promises');
const path = require('path');

const init = async () => {
  const server = Hapi.server({
    port: 4443,
    tls: {
      key: fs.readFileSync('server-key.pem'),
      cert: fs.readFileSync('server-cert.pem'),
      ca: fs.readFileSync('ca-cert.pem'),
      requestCert: true,
      rejectUnauthorized: true
    }
  });

  server.route({
    method: 'GET',
    path: '/reports/{filename}',
    options: {
      validate: {
        params: {
          filename: Joi.string().pattern(/^[a-zA-Z0-9_.-]+$/) // strict allowlist
        }
      }
    },
    handler: async (request, h) => {
      const safeName = request.params.filename;
      const base = path.resolve('/data/reports');
      const filePath = path.join(base, safeName);

      // Ensure path is within base directory (prevent path traversal)
      if (!filePath.startsWith(base)) {
        return h.response('Forbidden').code(403);
      }

      try {
        const data = await readFile(filePath, 'utf8');
        return h.response(data);
      } catch (err) {
        return h.response('Not found').code(404);
      }
    }
  });

  await server.start();
};
init();

This approach eliminates shell injection by avoiding exec/spawn for simple file reads and uses strict pattern validation for the filename.

2. If shell invocation is unavoidable, use strict allowlisting and avoid concatenation

When you must invoke a CLI tool, pass arguments as an array and validate each parameter against an allowlist. Do not pass raw user input to the command string.

const Hapi = require('@hapi/hapi');
const { spawn } = require('child_process');

const ALLOWED_COMMANDS = new Set(['generate', 'export', 'info']);

const init = async () => {
  const server = Hapi.server({
    port: 4443,
    tls: {
      key: fs.readFileSync('server-key.pem'),
      cert: fs.readFileSync('server-cert.pem'),
      ca: fs.readFileSync('ca-cert.pem'),
      requestCert: true,
      rejectUnauthorized: true
    }
  });

  server.route({
    method: 'POST',
    path: '/cli/{action}',
    options: {
      validate: {
        params: {
          action: Joi.string().valid(...Array.from(ALLOWED_COMMANDS)) // strict allowlist
        }
      }
    },
    handler: (request, h) => {
      const action = request.params.action;
      if (!ALLOWED_COMMANDS.has(action)) {
        return h.response('Invalid action').code(400);
      }

      // Safe: arguments passed as array, no shell involved
      const child = spawn('mycli', [action, '--output', '/tmp/out.json']);

      child.on('close', (code) => {
        // handle completion
      });

      return h.response('Queued').code(202);
    }
  });

  await server.start();
};
init();

Using spawn with an array and a predefined allowlist prevents command injection even if mTLS authenticates the client. mTLS configuration ensures only clients with valid certificates can reach the endpoint, but input validation remains mandatory.

3. mTLS server configuration best practices

Ensure your Hapi TLS options enforce client verification and do not inadvertently permit unauthenticated or improperly authenticated requests.

const tlsOptions = {
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem'),
  ca: fs.readFileSync('ca-cert.pem'),
  requestCert: true,
  rejectUnauthorized: true
};

const server = Hapi.server({
  port: 4443,
  tls: tlsOptions
});

With requestCert: true and rejectUnauthorized: true, only clients presenting a certificate signed by the trusted CA are accepted. Combine this with application-level validation to defend against authorized-but-malicious or buggy clients.

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 alone prevent Command Injection in Hapi?
No. Mutual TLS authenticates the client and server but does not validate or sanitize application input. Command Injection must be prevented through strict input validation, allowlists, and avoiding shell execution.
What is a safe pattern for handling user input in Hapi when using mTLS?
Use strict allowlists (e.g., Joi patterns or enumerations), avoid shell commands, prefer native APIs (fs, path), and if shell use is unavoidable, pass arguments as an array with spawn while never concatenating raw user input.