Command Injection in Rails with Mutual Tls
Command Injection in Rails with Mutual Tls — how this specific combination creates or exposes the vulnerability
Command injection occurs when untrusted input is concatenated into a system command executed by the application. In Ruby on Rails, common patterns include system, exec, %x{...}, and Open3.*. When mutual TLS (mTLS) is used, developers sometimes mistakenly believe that transport-layer authentication reduces application-layer risks, leading to relaxed input validation around commands that are invoked while mTLS is active (e.g., during certificate renewal, revocation checks, or integration with internal PKI tooling).
With mTLS, the server presents a certificate and the client presents a certificate; this is enforced at the TLS layer by the web server (e.g., Puma behind nginx) or the application itself via a custom SSL context. However, mTLS does not sanitize or validate data that flows into shell commands. An attacker who can control a parameter that ends up in a shell command can exploit command injection regardless of mTLS being used, because mTLS does not restrict what the application does with that input.
Consider a Rails integration that uses mTLS to call an internal service and then runs a command to convert a downloaded certificate file using openssl:
cert_name = params[:cert_name]
`openssl x509 -in #{cert_name} -text -noout`
Here, cert_name is directly interpolated into a shell command. Even if the request arrived over an mTLS-encrypted channel, the command injection remains exploitable. The mTLS context ensures the request is authenticated at the transport level, but it does nothing to prevent the attacker from injecting shell metacharacters (e.g., ;, &&, |) to execute arbitrary commands. This creates a false sense of security: teams may focus on certificate validation and cipher suites while neglecting input sanitization for shell commands.
Similarly, using system with multiple arguments does not automatically protect you if you pass a single concatenated string or use shell expansion:
system("openssl verify -CAfile " + ca_file_path)
If ca_file_path is user-influenced and contains shell metacharacters, command injection is possible. mTLS may be used to authenticate the client to the CA repository service, but it does not mitigate command injection in the Rails process.
Rails-specific nuances include the use of ActiveSupport::Dependencies and autoloading paths that can be abused via path traversal combined with command injection. For example, an attacker might manipulate a filename to include shell commands if the filename is used unsafely in backticks or system calls.
Because mTLS operates at the transport layer, it is orthogonal to command injection. Security assessments must test command injection independently of mTLS configurations, validating that inputs are properly escaped and that shell commands are avoided in favor of safer APIs.
Mutual Tls-Specific Remediation in Rails — concrete code fixes
To remediate command injection in Rails when mTLS is in use, focus on input validation, avoiding shell metacharacters, and using language constructs that do not invoke a shell. mTLS should be treated as a transport safeguard, not an application input safeguard.
1. Use Open3.capture3 or IO.popen with explicit arguments
Instead of shell interpolation, pass command and arguments as an array. This bypasses shell interpretation entirely:
require 'open3'
cert_name = params[:cert_name]
# Safe: arguments are passed directly, no shell involved
stdout, stderr, status = Open3.capture3("openssl", "x509", "-in", cert_name, "-text", "-noout")
If you must use shell syntax (e.g., for pipes or redirects), sanitize input strictly with a whitelist:
allowed_names = Set.new(["server.crt", "intermediate.crt", "ca.crt"])
cert_name = params[:cert_name]
if allowed_names.include?(cert_name)
`openssl x509 -in #{cert_name} -text -noout`
else
raise "Invalid certificate name"
end
2. Validate and restrict file paths for mTLS operations
When mTLS requires reading certificate files, ensure paths are resolved within a controlled directory and do not contain shell metacharacters or directory traversal sequences:
base_dir = Rails.root.join("certs").freeze
requested = params[:cert_name]
# Prevent path traversal and shell injection
filename = File.basename(requested) # strips directory components
cert_path = File.join(base_dir, filename)
if File.exist?(cert_path)
`openssl x509 -in #{cert_path.shellescape} -text -noout`
else
raise "Certificate not found"
end
Note the use of shellescape from Shellwords if you must construct a shell string. Prefer array-based invocation as shown earlier.
3. Secure mTLS setup in Rails using a custom SSL context
When configuring mTLS in Rails (e.g., for outbound HTTP clients), use Ruby's OpenSSL::SSL::SSLContext to present client certificates without invoking shell commands:
require 'net/http'
require 'openssl'
ctx = OpenSSL::SSL::SSLContext.new
ctx.key = OpenSSL::PKey::RSA.new(File.read("/path/client.key"))
ctx.cert = OpenSSL::X509::Certificate.new(File.read("/path/client.crt"))
ctx.ca_file = "/path/ca.crt"
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
http = Net::HTTP.new("internal.service", 443)
http.use_ssl = true
http.ssl_context = ctx
request = Net::HTTP::Get.new("/verify")
response = http.request(request)
This approach keeps mTLS configuration inside Ruby and avoids shell usage. Do not construct command-line strings to invoke tools like openssl for TLS setup; use native libraries instead.
4. Framework-level hardening
Ensure Rails parameter filtering does not inadvertently log or expose sensitive data that could be used in command injection. Use strong parameters and avoid passing raw user input to system calls:
def cert_params
params.require(:cert).permit(:name)
end
Combine this with input validation libraries or custom validators to enforce allowed characters (e.g., alphanumeric and dashes only) for identifiers used in any subprocess invocation.
mTLS enhances authentication and encryption in transit but does not change how Rails processes input for shell commands. Remediation is about secure coding practices, not TLS settings.
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 |