Command Injection with Mutual Tls
How Command Injection Manifests in Mutual TLS
Mutual TLS (mTLS) adds client‑certificate verification to the standard TLS handshake. When developers need to validate a client certificate outside of the language’s TLS library, they sometimes invoke external tools such as openssl or keytool and concatenate user‑supplied data (e.g., a hostname, certificate PEM, or serial number) into a shell command. If that data is not strictly validated or escaped, an attacker can inject arbitrary shell commands.
Typical vulnerable patterns include:
- Building an
openssl verifycommand where the client‑certificate PEM is taken directly from a request header or JSON field and inserted into the command string. - Using a hostname supplied by the caller in an
openssl s_client -connectcall to check the server’s certificate chain, without sanitizing the hostname for shell metacharacters. - Parsing a client certificate’s serial number or subject with
openssl x509 -noout -serialand concatenating the value into a larger script that logs or processes the result.
Because mTLS is often used in internal APIs or service‑to‑service communication, developers may assume the caller is trusted and skip input validation, opening a command‑injection vector that can lead to full host compromise.
Mutual TLS‑Specific Detection
middleBrick performs unauthenticated, black‑box scanning of the API surface. When scanning an endpoint that expects mTLS, the scanner:
- Attempts a normal TLS handshake with a valid client certificate to establish baseline behavior.
- Injects payloads into fields that are likely to be used in external command construction (e.g., custom headers, query parameters, JSON properties labeled “cert”, “hostname”, “cn”, “serial”).
- Uses a set of command‑injection probes such as
; id,$(whoami),|| ls -la, and`sleep 5`to detect timing or output differences. - Monitors for changes in response status, body, or response time that indicate the injected command was executed.
If the API reflects the output of the injected command (e.g., returns the UID from id) or shows a delayed response consistent with a sleep, middleBrick flags the finding as a command‑injection vulnerability under the "Input Validation" category, providing the exact payload that triggered the behavior and remediation guidance.
Example CLI usage:
middlebrick scan https://api.example.com/mtls-endpoint
The command returns a JSON report that includes the command‑injection finding, severity, and suggested fixes.
Mutual TLS‑Specific Remediation
The most reliable fix is to avoid invoking external commands altogether and perform all certificate validation using the language’s native TLS library. When external tooling is unavoidable, use safe subprocess APIs that accept arguments as an array and never invoke a shell.
Node.js (using tls module)
const tls = require('tls');
const fs = require('fs');
const options = {
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
ca: fs.readFileSync('ca.pem'), // trust store for client certs
requestCert: true, // require client cert
rejectUnauthorized: true // fail if client cert not trusted
};
const server = tls.createSecureServer(options, (socket) => {
// socket.getPeerCertificate() provides the verified client cert
const cert = socket.getPeerCertificate();
// Use cert properties directly; no shell needed
socket.write(`Hello ${cert.subject.CN}\n`);
socket.end();
});
server.listen(8443);
Go (using crypto/tls)
package main
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
"net/http"
)
func main() {
caCert, _ := ioutil.ReadFile("ca.pem")
caPool := x509.NewCertPool()
caPool.AppendCertsFromPEM(caCert)
serverCert, _ := tls.LoadX509KeyPair("server-cert.pem", "server-key.pem")
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caPool,
}
server := &http.Server{
Addr: ":8443",
TLSConfig: tlsConfig,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil || len(r.TSL.PeerCertificates) == 0 {
http.Error(w, "client cert required", http.StatusBadRequest)
return
}
cert := r.TLS.PeerCertificates[0]
w.Write([]byte("Hello " + cert.Subject.CommonName + "\n"))
}),
}
log.Fatal(server.ListenAndServeTLS("", "")) // cert/key already in tlsConfig
}
Python (using ssl.SSLContext)
import ssl
import socket
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile="server-cert.pem", keyfile="server-key.pem")
context.load_verify_locations(cafile="ca.pem")
context.verify_mode = ssl.CERT_REQUIRED # demand client cert
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(('0.0.0.0', 8443))
sock.listen(5)
with context.wrap_socket(sock, server_side=True) as ssock:
conn, addr = ssock.accept()
# conn is an SSLSocket; client cert is already validated
cert = conn.getpeercert()
conn.sendall(f'Hello {cert.get("subject", [[('', 'CN')]])[0][1]}\n'.encode())
conn.close()
If you must call an external tool (e.g., for legacy reasons), never build a command string with user input. Instead, pass data as separate arguments or via environment variables, and avoid shell=True.
Unsafe (example to avoid)
# DO NOT DO THIS
cmd = f'openssl verify -CAfile ca.pem {user_supplied_cert}'
os.system(cmd)
Safe alternative (Node.js)
const { execFile } = require('child_process');
execFile('openssl', ['verify', '-CAfile', 'ca.pem', certPath], (err, stdout, stderr) => {
// handle result
});
By eliminating shell concatenation and relying on proven TLS libraries, you remove the command‑injection surface while preserving the security benefits of mutual TLS.
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 |