Command Injection via HTTP Request Parameters

Critical Risk command-injection
javacommand-injectionprocessbuilderruntime-execrce

What it is

Command injection vulnerabilities occur when HTTP request data is passed to ProcessBuilder or Runtime.exec() without proper validation, allowing attackers to execute arbitrary operating system commands.

@RestController
public class SystemController {
    
    @GetMapping("/api/ping")
    public ResponseEntity<String> pingHost(@RequestParam String host) {
        try {
            // VULNERABLE: Direct user input in command
            Process process = Runtime.getRuntime().exec("ping -c 4 " + host);
            
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()));
            StringBuilder output = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }
            
            return ResponseEntity.ok(output.toString());
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Ping failed");
        }
    }
    
    @PostMapping("/api/backup")
    public ResponseEntity<String> createBackup(@RequestBody Map<String, String> request) {
        String filename = request.get("filename");
        String directory = request.get("directory");
        
        try {
            // VULNERABLE: User input in ProcessBuilder
            ProcessBuilder pb = new ProcessBuilder("tar", "-czf", filename, directory);
            Process process = pb.start();
            
            int exitCode = process.waitFor();
            return exitCode == 0 
                ? ResponseEntity.ok("Backup created") 
                : ResponseEntity.status(500).body("Backup failed");
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Backup failed");
        }
    }
}
@RestController
public class SecureSystemController {
    
    private static final Set<String> ALLOWED_HOSTS = Set.of(
        "localhost", "google.com", "github.com"
    );
    
    private static final Pattern HOSTNAME_PATTERN = 
        Pattern.compile("^[a-zA-Z0-9][a-zA-Z0-9.-]{0,61}[a-zA-Z0-9]$");
    
    @GetMapping("/api/ping")
    public ResponseEntity<String> pingHost(@RequestParam String host) {
        // SECURE: Validate against allowlist
        if (!ALLOWED_HOSTS.contains(host) || !isValidHostname(host)) {
            return ResponseEntity.badRequest().body("Invalid host");
        }
        
        try {
            // SECURE: Use ProcessBuilder with separate arguments
            ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", host);
            pb.redirectErrorStream(true);
            Process process = pb.start();
            
            // Set timeout to prevent hanging
            boolean finished = process.waitFor(30, TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                return ResponseEntity.status(500).body("Timeout");
            }
            
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()));
            StringBuilder output = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }
            
            return ResponseEntity.ok(output.toString());
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Ping failed");
        }
    }
    
    private boolean isValidHostname(String host) {
        return host != null && HOSTNAME_PATTERN.matcher(host).matches();
    }
}

💡 Why This Fix Works

The secure version validates input against an allowlist, uses ProcessBuilder with separate arguments instead of string concatenation, and implements timeout protection.

Why it happens

Java applications pass HTTP request parameters (from ServletRequest.getParameter(), @RequestParam, etc.) directly to ProcessBuilder or Runtime.exec() without validation. User-controlled input becomes part of executed system commands, allowing attackers to inject malicious command sequences.

Root causes

Direct HTTP Parameters to Command Execution

Java applications pass HTTP request parameters (from ServletRequest.getParameter(), @RequestParam, etc.) directly to ProcessBuilder or Runtime.exec() without validation. User-controlled input becomes part of executed system commands, allowing attackers to inject malicious command sequences.

Missing Input Validation and Sanitization

No validation or sanitization applied to user input before including it in command execution. Applications don't check for shell metacharacters (;, |, &, $, `, \) or validate input against expected patterns, allowing command injection through crafted requests.

String Concatenation for Command Building

System commands built using string concatenation (String.format(), StringBuilder, or + operator) to combine user input with command templates. This creates single command strings where special characters can modify command structure and enable injection.

Absent Parameter Allowlists

No allowlists restrict user-controllable command parameters to known-safe values. Applications accept arbitrary user input as command arguments without validating against permitted options, filenames, or paths, enabling attackers to pass malicious parameters.

Fixes

1

Never Pass User Input Directly to Commands

Design applications to completely avoid passing user-controlled data to system command execution. If user input is necessary, validate it against strict allowlists of permitted values before use. Prefer architectural designs that eliminate the need for external command execution entirely.

2

Implement Strict Allowlist Validation

Create and enforce allowlists of permitted values for any user-controllable command parameters. Use switch statements or Map lookups to validate input matches exactly one approved option. Reject any input that doesn't exactly match an allowlist entry, including variations in case or whitespace.

3

Use ProcessBuilder with Separate Arguments

Prefer ProcessBuilder over Runtime.exec() and pass command and arguments as separate array elements: new ProcessBuilder('command', arg1, arg2). This prevents shell interpretation of special characters. Never use ProcessBuilder with a single concatenated command string.

4

Replace Commands with Native Java APIs

Eliminate system command execution by using native Java libraries and APIs wherever possible. For file operations use java.nio.file, for compression use java.util.zip, for image processing use javax.imageio. Native APIs are faster, more secure, and platform-independent.

5

Apply Restrictive Regex Pattern Validation

If user input must be included in commands after allowlist checks fail, validate with extremely restrictive regex patterns (e.g., ^[a-zA-Z0-9_-]+$). Reject any input containing shell metacharacters, whitespace, or special characters. Implement multiple validation layers for defense-in-depth.

Detect This Vulnerability in Your Code

Sourcery automatically identifies command injection via http request parameters and many other security issues in your codebase.