Skip to content

CWE-77: Command Injection - Java

Overview

Command injection in Java occurs when applications construct system commands using untrusted input without proper sanitization. Attackers inject shell metacharacters to execute arbitrary commands with the application's privileges.

Primary Defence: Use ProcessBuilder with separate arguments instead of Runtime.exec() with concatenated strings, avoid shell invocation by not using shell interpreters (sh -c, cmd /c), validate all user input against strict allowlists before using in commands, and use Java libraries or APIs for specific tasks instead of executing system commands to prevent command injection attacks.

Common Vulnerable Patterns

Runtime.exec() with String Concatenation

// VULNERABLE - User input in command string
String userIP = request.getParameter("ip");
Runtime.getRuntime().exec("ping " + userIP);

// Attack: ip=8.8.8.8; cat /etc/passwd
// Executes: ping 8.8.8.8; cat /etc/passwd

ProcessBuilder with Shell Invocation

// VULNERABLE - Using shell with user input
String domain = request.getParameter("domain");
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", "nslookup " + domain);
pb.start();

// Attack: domain=example.com && whoami
// Executes: nslookup example.com && whoami

Runtime.exec() with Shell Wrapper

// VULNERABLE - cmd.exe wrapper
String fileName = request.getParameter("file");
Runtime.getRuntime().exec("cmd.exe /c type " + fileName);

// Attack: file=data.txt & del /F /Q *.*
// Executes: type data.txt & del /F /Q *.*

Secure Patterns

Use Java Native APIs (Primary Defense)

// SECURE - Use Java APIs instead of system commands

// Instead of: exec("ping " + host)
import java.net.InetAddress;

public boolean isHostReachable(String host) throws IOException {
    if (!host.matches("^[a-zA-Z0-9.-]+$")) {
        throw new IllegalArgumentException("Invalid hostname");
    }
    InetAddress address = InetAddress.getByName(host);
    return address.isReachable(5000); // 5 second timeout
}

// Instead of: exec("rm " + file)
import java.nio.file.Files;
import java.nio.file.Paths;

public void deleteFile(String fileName) throws IOException {
    // Validate filename
    if (!fileName.matches("^[a-zA-Z0-9_.-]+$")) {
        throw new IllegalArgumentException("Invalid filename");
    }

    Path filePath = Paths.get("/safe/directory", fileName);
    Files.delete(filePath);
}

// Instead of: exec("curl " + url)
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;

public String fetchUrl(String url) throws IOException, InterruptedException {
    HttpClient client = HttpClient.newHttpClient();
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(url))
        .build();
    HttpResponse<String> response = client.send(request, 
        HttpResponse.BodyHandlers.ofString());
    return response.body();
}

Why this works:

  • Native Java APIs eliminate shell execution: InetAddress.isReachable() uses ICMP, Files.delete() uses syscalls, HttpClient uses sockets
  • No attack surface for shell metacharacters: Bypassing shell removes risk of ;, &, |, backticks, $()
  • Regex validation provides defense-in-depth: Pattern blocks path traversal, command separators, null bytes
  • Path construction prevents directory traversal: Paths.get("/safe/directory", fileName) combines trusted base with user input
  • Type-safe APIs prevent injection: HttpClient with URI, Files with Path avoid string-based vulnerabilities
  • Cross-platform and maintainable: Works on Windows/Linux/macOS without handling shell-specific differences

Use ProcessBuilder with Argument Array

// SECURE - No shell invocation, arguments separated
public void securePing(String ipAddress) throws IOException {
    // Validate IP address format
    if (!ipAddress.matches("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" +
                           "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) {
        throw new IllegalArgumentException("Invalid IP address");
    }

    // Use argument array - no shell interpretation
    ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", ipAddress);

    // Prevent shell invocation
    pb.redirectErrorStream(true);

    Process process = pb.start();

    // Read output safely
    try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(process.getInputStream()))) {
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
    }
}

Why this works:

  • ProcessBuilder with separate arguments prevents shell interpretation: Each argument is distinct element, not shell script
  • Direct executable invocation: Uses fork()/exec() (Linux) or CreateProcess() (Windows) without shell wrapper
  • Metacharacters become literal data: ;, &, |, $() treated as strings in argument, not command separators
  • Strict IP validation blocks injection: Regex ensures only valid IPs pass, blocking attempts like 8.8.8.8; cat /etc/passwd
  • redirectErrorStream(true) simplifies output: Merges stderr into stdout, prevents information leaks from separate errors
  • Superior to Runtime.exec(String): Single-string form may invoke shell depending on platform/JVM version

Runtime.exec() with String Array

// SECURE - Array form prevents shell interpretation
public void secureNslookup(String domain) throws IOException {
    // Validate domain name
    if (!domain.matches("^[a-zA-Z0-9.-]+$")) {
        throw new IllegalArgumentException("Invalid domain");
    }

    // Use String[] array, not single string
    String[] command = {"nslookup", domain};
    Process process = Runtime.getRuntime().exec(command);

    // Handle output
    try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(process.getInputStream()))) {
        reader.lines().forEach(System.out::println);
    }
}

Why this works:

  • Runtime.exec(String[]) array form prevents shell interpretation: First element is executable, rest are literal arguments
  • Contrasts with vulnerable single-string form: Runtime.exec(String) may invoke shell on some platforms
  • Domain validation blocks injection: Regex prevents shell metacharacters, path traversal, command separators
  • No shell wrapper means literal characters: ;, &, |, backticks have no special meaning, passed to nslookup as-is
  • Stream-based output prevents memory exhaustion: Line-by-line processing handles large outputs safely
  • ProcessBuilder preferred for better control: More explicit about avoiding shell, easier security auditing

Strict Input Validation

// SECURE - Allowlist validation before any command execution
import java.util.regex.Pattern;

public class CommandValidator {
    private static final Pattern HOSTNAME_PATTERN = 
        Pattern.compile("^[a-zA-Z0-9.-]+$");
    private static final Pattern IP_PATTERN = 
        Pattern.compile("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" +
                       "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
    private static final Pattern FILENAME_PATTERN = 
        Pattern.compile("^[a-zA-Z0-9_.-]+$");

    public static String validateHostname(String input) {
        if (input == null || !HOSTNAME_PATTERN.matcher(input).matches()) {
            throw new IllegalArgumentException("Invalid hostname format");
        }
        if (input.length() > 253) {
            throw new IllegalArgumentException("Hostname too long");
        }
        return input;
    }

    public static String validateIPAddress(String input) {
        if (input == null || !IP_PATTERN.matcher(input).matches()) {
            throw new IllegalArgumentException("Invalid IP address format");
        }
        return input;
    }

    public static String validateFilename(String input) {
        if (input == null || !FILENAME_PATTERN.matcher(input).matches()) {
            throw new IllegalArgumentException("Invalid filename format");
        }
        if (input.contains("..") || input.contains("/") || input.contains("\\")) {
            throw new IllegalArgumentException("Path traversal attempt detected");
        }
        return input;
    }
}

Why this works:

  • Centralized validation: Compiled Pattern objects (thread-safe, performant) ensure consistency - every code path enforcing same strict rules across all command execution
  • Allowlist regex: ^[a-zA-Z0-9.-]+$ blocks shell metacharacters (;, &, |, >, <, backticks, $(), newlines, null bytes) by only permitting safe chars
  • Specific checks: Path traversal detection (contains(".."), /, \\), length limits (≤253 per RFC 1035), null checks prevent directory traversal, DoS, NullPointerException
  • Fail-closed exceptions: throw new IllegalArgumentException rejects suspicious input rather than sanitizing (error-prone)
  • Defense-in-depth: Complements argument arrays - validates input before ProcessBuilder, prevents injection even if developer accidentally uses Runtime.exec(String)

Common Java Command Injection Scenarios

File Conversion

// VULNERABLE
String file = request.getParameter("file");
Runtime.getRuntime().exec("convert " + file + " output.pdf");

// SECURE - Use Apache PDFBox or iText library
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;

public void convertToPdf(String imageFile) throws IOException {
    // Validate filename
    String safeName = CommandValidator.validateFilename(imageFile);

    // Use PDF library instead of system command
    PDDocument document = new PDDocument();
    // ... conversion logic using PDFBox
    document.save("output.pdf");
    document.close();
}

Archive Operations

// VULNERABLE
String files = request.getParameter("files");
Runtime.getRuntime().exec("tar -czf archive.tar.gz " + files);

// SECURE - Use Apache Commons Compress
import org.apache.commons.compress.archivers.tar.*;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;

public void createArchive(List<String> fileNames) throws IOException {
    try (FileOutputStream fos = new FileOutputStream("archive.tar.gz");
         GzipCompressorOutputStream gzos = new GzipCompressorOutputStream(fos);
         TarArchiveOutputStream taos = new TarArchiveOutputStream(gzos)) {

        for (String fileName : fileNames) {
            // Validate each filename
            String safeName = CommandValidator.validateFilename(fileName);
            File file = new File(safeName);

            TarArchiveEntry entry = new TarArchiveEntry(file, safeName);
            taos.putArchiveEntry(entry);
            Files.copy(file.toPath(), taos);
            taos.closeArchiveEntry();
        }
    }
}

Network Diagnostics

// VULNERABLE
String host = request.getParameter("host");
Runtime.getRuntime().exec("traceroute " + host);

// SECURE - Limited to essential validation
public void secureTraceroute(String host) throws IOException {
    // Validate hostname
    String validHost = CommandValidator.validateHostname(host);

    // Use argument array
    ProcessBuilder pb;
    if (System.getProperty("os.name").toLowerCase().contains("windows")) {
        pb = new ProcessBuilder("tracert", "-h", "30", validHost);
    } else {
        pb = new ProcessBuilder("traceroute", "-m", "30", validHost);
    }

    // Set timeout
    pb.redirectErrorStream(true);
    Process process = pb.start();

    // Wait with timeout
    if (!process.waitFor(60, TimeUnit.SECONDS)) {
        process.destroy();
        throw new IOException("Traceroute timeout");
    }
}

Verification

After implementing the recommended secure patterns, verify the fix through multiple approaches:

  • Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
  • Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
  • Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
  • Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
  • Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
  • Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
  • Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
  • Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced

Additional Resources