HIGH command injectionsinatrahmac signatures

Command Injection in Sinatra with Hmac Signatures

Command Injection in Sinatra with Hmac Signatures — how this specific combination creates or exposes the vulnerability

Command injection occurs when an attacker can cause an application to execute arbitrary operating system commands. In Sinatra, this risk can emerge when HMAC signatures are used to validate incoming requests but the application uses the signature data in an unsafe way before verification or in constructing shell commands. A common pattern is to include user-controlled values in a signature to ensure integrity, then using those values in a system call without proper sanitization. If the server-side code builds a shell command by string concatenation—for example, passing a user-supplied parameter directly into system, exec, or backticks—attackers can escape the expected argument boundaries and inject additional shell commands.

Consider a Sinatra endpoint that expects an HMAC signature in a header and a parameter filename that identifies a file to process. If the server recomputes the HMAC over the raw parameter and compares it to the client-supplied signature, the signature itself may be valid even when the parameter is malicious. The vulnerability is not in the HMAC algorithm but in what the server does after verification. If the code then runs something like system("tar -xvf #{filename}"), an attacker can supply file.tar; rm -rf / to execute arbitrary commands. Because the signature covers the attacker-controlled input, the server may treat the request as legitimate, bypassing any naive allow-list assumptions about the parameter format.

HMAC signatures do not prevent command injection; they only prove that the message has not been altered. Attackers might also probe for endpoints that accept signed but unverified inputs, especially if the application logs or reflects the parameter values. In a black-box scan, such endpoints can be identified by sending crafted HMACs with shell metacharacters and observing command execution behavior, error messages, or side effects. This is why it is essential to treat all inputs as untrusted even when protected by a signature and to apply strict input validation and output encoding before using any data in a shell context.

Hmac Signatures-Specific Remediation in Sinatra — concrete code fixes

To mitigate command injection while using HMAC signatures in Sinatra, ensure that user-controlled data is never used directly in shell commands. Instead, use strict allow-listing, avoid shell metacharacters, and prefer safe APIs that do not invoke a shell. Below are two concrete patterns: one vulnerable and one secure.

Vulnerable pattern

require 'sinatra'
require 'openssl'

SECRET = 'shared-secret'

post '/process' do
  filename = params['filename']
  received_hmac = request.env['HTTP_X_HMAC']
  computed_hmac = OpenSSL::HMAC.hexdigest('SHA256', SECRET, filename)
  if timing_safe_compare(computed_hmac, received_hmac)
    # Dangerous: filename is used in a shell command
    result = `tar -xvf #{filename}`
    result
  else
    halt 401, 'Invalid signature'
  end
end

def timing_safe_compare(a, b)
  return false unless a.bytesize == b.bytesize
  l = a.unpack 'C*'
  r = 0
  b.each_byte { |c| r |= c ^ l.shift }
  r == 0
end

In the vulnerable example, the filename is interpolated into a backtick command after a valid HMAC is confirmed. An attacker can supply a filename such as archive.tar; id to execute arbitrary commands.

Secure remediation

require 'sinatra'
require 'openssl'

SECRET = 'shared-secret'
ALLOWED_BASENAMES = ['report.tar', 'data.tar', 'logs.tar'].freeze

def safe_filename(input)
  # Strict allow-list: only exact matches from a pre-approved set
  ALLOWED_BASENAMES.include?(input) ? input : nil
end

post '/process' do
  filename = params['filename']
  received_hmac = request.env['HTTP_X_HMAC']
  computed_hmac = OpenSSL::HMAC.hexdigest('SHA256', SECRET, filename)
  unless timing_safe_compare(computed_hmac, received_hmac)
    halt 403, 'Invalid signature'
  end

  safe = safe_filename(filename)
  halt 400, 'Filename not allowed' unless safe

  # Safe: no shell interpolation, explicit arguments
  result = `tar -xvf #{safe.shellescape}`
  result
end

def timing_safe_compare(a, b)
  return false unless a.bytesize == b.bytesize
  l = a.unpack 'C*'
  r = 0
  b.each_byte { |c| r |= c ^ l.shift }
  r == 0
end

Key improvements:

  • Allow-list validation: Only pre-approved filenames are accepted, preventing unexpected or malicious inputs.
  • No shell interpolation: Even with a valid HMAC, the code uses backticks with a shell-escaped argument. In this example, explicit allow-listing makes shellescape a defense-in-depth measure; ideally, you would avoid a shell entirely by using a Ruby tar library (e.g., tar-win32 or ruby-tar) to avoid shell involvement altogether.
  • Explicit error handling: Clear 400 and 403 responses help clients understand rejection reasons without leaking sensitive details.

If you must construct shell commands, always use built-in escaping (e.g., shellescape from shellwords) and avoid interpolating untrusted data. Better yet, use language-native libraries for archive extraction to eliminate the shell surface area entirely.

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 a valid HMAC protect against command injection in Sinatra?
No. HMAC signatures verify integrity and authenticity of data, but they do not prevent command injection. If the server uses the signed input in a shell command without strict validation and escaping, an attacker can still inject commands.
What is the best way to handle user input in Sinatra when HMAC is required?
Use strict allow-listing for expected values, avoid using user input directly in shell commands, prefer native language libraries over shell invocation, and apply shell escaping (e.g., Shellwords.shellescape) as a defense-in-depth measure even after signature verification.