Command Injection in Rails with Api Keys
Command Injection in Rails with Api Keys — how this specific combination creates or exposes the vulnerability
Command Injection occurs when an application passes untrusted input directly to a system shell or to an OS command builder. In Ruby on Rails, using API keys in a way that feeds user-controlled data into command execution is a common root cause. For example, reading an API key from environment variables is safe, but concatenating that key into a shell command string with user input creates a path for injection.
Consider a Rails service object that calls an external API client. If the implementation builds a shell command using string interpolation and passes along an API key taken from ENV alongside untrusted parameters, an attacker can manipulate the command line. A vulnerable pattern might look like:
api_key = ENV['EXTERNAL_API_KEY']
system("curl -H 'Authorization: Bearer #{api_key}' https://api.example.com/v1/search?q=#{params[:query]}")
An attacker providing query as test; cat /etc/passwd can cause the shell to execute additional commands. Even if the API key itself is not secret in this context (e.g., it is treated as a bearer token), injection can alter the intended request, leak data, or run arbitrary commands with the privileges of the Rails process.
A second variant involves using backticks or %x with interpolated API keys and user input. For instance:
output = `#{ENV['EXTERNAL_API_KEY']} --query "#{params[:query]}"`
Here, the API key is used as part of the command executable or flag, and the query is interpolated without sanitization. This demonstrates how an API key does not need to be secret to contribute to a command injection flaw; its presence in a constructed shell command alongside untrusted input is enough to create the vulnerability.
Additionally, Rails developers may use libraries or wrappers that shell out for API interactions. If these libraries are invoked with interpolated strings that include API keys and uncontrolled data, the injection surface is effectively the same as raw system calls. The risk is not about exposing the key itself, but about allowing an attacker to change the command structure, potentially escalating impact by leveraging the permissions of the Rails runtime.
Api Keys-Specific Remediation in Rails — concrete code fixes
Secure handling of API keys in Rails requires avoiding shell construction entirely and using safe HTTP clients. The core remediation is to never interpolate API keys or any external data into shell commands. Instead, use language-native HTTP libraries or well-maintained gems that do not rely on shell execution.
Replace system or backtick calls with a Ruby HTTP client such as Net::HTTP or Faraday. This eliminates the shell as an intermediary and removes injection risk. For example, using Net::HTTP:
require 'net/http'
require 'uri'
api_key = ENV['EXTERNAL_API_KEY']
uri = URI('https://api.example.com/v1/search')
uri.query = URI.encode_www_form(q: params[:query])
request = Net::HTTP::Get.new(uri)
request['Authorization'] = "Bearer #{api_key}"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
puts response.body
With Faraday, the code is even cleaner and more configurable:
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.request :url_encoded
faraday.response :logger
faraday.adapter :net_http
end
api_key = ENV['EXTERNAL_API_KEY']
resp = conn.get do |req|
req.url '/v1/search'
req.params[:q] = params[:query]
req.headers['Authorization'] = "Bearer #{api_key}"
end
puts resp.body
These approaches keep the API key in HTTP headers or parameters managed by the library, avoiding any shell interpolation. They also provide better error handling, timeouts, and SSL management compared with manual shell invocation.
If you must construct command-like invocations (for example, calling a local binary that requires an API key as an argument), use Ruby’s spawn with an argument array and pass the key as a separate element, bypassing shell interpretation:
api_key = ENV['EXTERNAL_API_KEY']
pid = spawn(['/usr/bin/curl', '--header', "Authorization: Bearer #{api_key}", 'https://api.example.com/v1/search'], out: '/tmp/curl.out')
Process.wait(pid)
Even here, prefer passing data via environment variables or configuration rather than embedding sensitive values in argument strings where possible. For continuous scanning in development and CI/CD, integrate middleBrick to detect command injection patterns; you can add API security checks to your CI/CD pipeline with the GitHub Action and fail builds if risky patterns are found.
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 |