Skip to main content

Overview

Command Injection (or OS Command Injection) allows an attacker to execute arbitrary commands on the host operating system via a vulnerable application. This flaw occurs when an application passes unsanitized user-supplied data to a system shell. By injecting command separators like ;, &&, or |, an attacker can append their own commands to the legitimate one, which are then executed with the privileges of the application.

Business Impact

This is one of the most critical vulnerabilities, as it can lead to a full compromise of the application server. An attacker can read/write any file, install malware or ransomware, exfiltrate all data, and use the compromised server as a pivot point to attack the internal network.

Reference Details

CWE ID: CWE-78 OWASP Top 10 (2021): A03:2021 - Injection Severity: Critical

Framework-Specific Analysis and Remediation

The only truly safe way to prevent command injection is to avoid calling out to OS commands with user-supplied data. Almost every language provides safe, built-in libraries for functionality that developers might otherwise try to achieve via shell commands (e.g., file operations, network requests). If executing a command is absolutely unavoidable, the application must use APIs that execute processes directly without invoking a shell, passing each user-controlled input as a separate, distinct argument.
  • Python
  • Java
  • .NET(C#)
  • PHP
  • Node.js
  • Ruby

Framework Context

Python’s standard os.system() function is a direct conduit to the system shell and is extremely dangerous. The subprocess module is the modern, secure alternative, but it can still be made vulnerable if shell=True is used with untrusted input.

Vulnerable Scenario 1: A Video Conversion Service

A service uses the ffmpeg command-line tool to convert uploaded video files, taking conversion options from the user.
# video/services.py
import os

def convert_video(input_path, output_path, user_options):
    # DANGEROUS: The user's options are directly formatted into the command string.
    # An attacker could provide options like "; rm -rf /"
    command = f"ffmpeg -i {input_path} {user_options} {output_path}"
    os.system(command)

Vulnerable Scenario 2: Network Diagnostic Tool

An admin panel includes a tool to ping a user-supplied IP address.
# diagnostics/views.py
import subprocess

def ping_host(request):
    host = request.GET.get('host')
    # DANGEROUS: Using subprocess is good, but `shell=True` with formatted
    # strings reintroduces the vulnerability.
    output = subprocess.check_output(f"ping -c 4 {host}", shell=True)
    return HttpResponse(output, content_type="text/plain")

Mitigation and Best Practices

Use the subprocess module and pass the command and its arguments as a list of strings (e.g., ['ping', '-c', '4', host]). This invokes the program directly without a shell, ensuring that the user’s input is treated as a single, safe argument and cannot be interpreted by the shell.

Secure Code Example

# diagnostics/views.py (Secure Version)
import subprocess

def ping_host(request):
    host = request.GET.get('host')
    
    # A simple allow-list validation is also good practice
    if not is_valid_ip(host): # Assume is_valid_ip is a validation function
        return HttpResponse("Invalid host format.", status=400)
        
    try:
        # SAFE: The command and its arguments are passed as a list.
        # The host variable is treated as a single argument, even if it contains
        # shell metacharacters like ';' or '&&'.
        output = subprocess.check_output(
            ['ping', '-c', '4', host],
            stderr=subprocess.STDOUT,
            text=True,
            timeout=10
        )
        return HttpResponse(output, content_type="text/plain")
    except subprocess.CalledProcessError as e:
        return HttpResponse(f"Error executing command:\n{e.output}", status=500)
    except subprocess.TimeoutExpired:
        return HttpResponse("Command timed out.", status=500)

Testing Strategy

Write tests that pass payloads containing shell metacharacters as input. For the ping example, a payload could be 8.8.8.8; ls. The test should assert that the command output does not contain the results of the injected ls command and that the program likely exited with an error because “8.8.8.8; ls” is not a valid hostname.
# diagnostics/tests.py
def test_ping_host_command_injection(self):
    injection_payload = "8.8.8.8; ls -la /"
    
    response = self.client.get(reverse('ping-host'), {'host': injection_payload})
    
    self.assertEqual(response.status_code, 500) # The ping command will fail
    # The crucial check: the output of the malicious 'ls' command is not present.
    self.assertNotIn("total", response.content.decode().lower())
    self.assertNotIn("drwx", response.content.decode().lower())