Back to Blog
critical SEVERITY8 min read

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

A critical command injection vulnerability was discovered in `page-object/src/main/java/com/iluwatar/pageobject/App.java` where `Runtime.getRuntime().exec()` was used to launch a file using `cmd.exe` with a directly concatenated file path. An attacker who could control the `applicationFile` variable could inject shell metacharacters to execute arbitrary system commands with the privileges of the running Java process. The fix replaces the unsafe `exec()` call with a properly tokenized `ProcessBui

O
By Orbis AppSec
Published June 6, 2026Reviewed June 6, 2026

Answer Summary

This is a command injection vulnerability (CWE-78) in Java, found in `App.java` at line 80 of the `page-object` module. The root cause is passing a user-influenced string directly into `Runtime.getRuntime().exec()` as a single shell command string, which allows shell metacharacters (`&`, `|`, `;`) to break out of the intended command. The fix replaces the vulnerable call with `new ProcessBuilder("cmd.exe", "/c", "start", "", applicationFile.getAbsolutePath()).start()`, which passes arguments as discrete tokens and never invokes a shell parser, preventing metacharacter injection entirely.

Vulnerability at a Glance

cweCWE-78
fixReplaced with ProcessBuilder using discrete argument tokens, bypassing shell interpretation
riskAttacker executes arbitrary OS commands with JVM process privileges
languageJava
root causeFile path concatenated directly into a shell command string passed to Runtime.exec()
vulnerabilityCommand Injection via Runtime.exec() string concatenation

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

Summary

A critical command injection vulnerability was discovered in page-object/src/main/java/com/iluwatar/pageobject/App.java where Runtime.getRuntime().exec() was used to launch a file using cmd.exe with a directly concatenated file path. An attacker who could control the applicationFile variable could inject shell metacharacters to execute arbitrary system commands with the privileges of the running Java process. The fix replaces the unsafe exec() call with a properly tokenized ProcessBuilder invocation, eliminating the injection surface entirely.


Introduction

The App.java file in the page-object module handles application launch logic, including a fallback path for Windows systems where java.awt.Desktop is not supported. That fallback, buried in an else branch at line 80, contained a single line that created a critical security hole:

Runtime.getRuntime().exec("cmd.exe start " + applicationFile);

The variable applicationFile is concatenated directly into a command string and handed to the Windows shell for execution. If an attacker can influence the value of applicationFile — through a crafted file path, a configuration value, or any upstream input — they can inject shell metacharacters and execute arbitrary commands on the host system with the same privileges as the Java process.

This is a textbook example of CWE-78: OS Command Injection, and it is exactly the kind of subtle, single-line vulnerability that is easy to overlook during code review but trivial to exploit in practice.


The Vulnerability Explained

What went wrong

The core problem is the way Runtime.getRuntime().exec() handles its input when given a single String argument.

Many Java developers assume that passing a string to exec() is equivalent to running it in a terminal. That assumption is partially correct — and that partial correctness is what makes it dangerous. When you pass a single string, the JVM splits it on whitespace and passes the first token as the executable and the rest as arguments. However, the shell (cmd.exe in this case) is still invoked as the executable, and cmd.exe does interpret metacharacters in its arguments.

Here is the vulnerable line:

// VULNERABLE: applicationFile is concatenated directly into the command string
Runtime.getRuntime().exec("cmd.exe start " + applicationFile);

If applicationFile is something like:

C:\myapp\app.html

The command becomes:

cmd.exe start C:\myapp\app.html

That is the intended behavior. But if applicationFile is:

C:\myapp\app.html & net user hacker P@ssw0rd /add

The command becomes:

cmd.exe start C:\myapp\app.html & net user hacker P@ssw0rd /add

cmd.exe interprets & as a command separator. The second command — creating a new Windows user account — executes silently alongside the first.

Attack scenario specific to this code

In the page-object design pattern context, applicationFile represents the path to an application file that the page object framework needs to open. If this path is derived from a test configuration file, a database record, a URL parameter, or any other externally influenced source, an attacker who can write to that source can inject commands.

Consider a scenario where the application file path is read from a properties file that a low-privileged user can edit. The attacker sets the path to:

index.html & powershell -EncodedCommand <base64-encoded-reverse-shell>

When App.java runs on a build server or CI/CD pipeline (a common use case for page object test frameworks), the injected PowerShell command executes with the privileges of the CI agent — potentially giving the attacker a foothold on the build infrastructure.

Why this is rated Critical

  • No validation or sanitization of applicationFile before it is used in the command
  • Shell metacharacters (&, |, ;, &&, ||, `) are not stripped or escaped
  • cmd.exe is explicitly invoked, meaning Windows shell interpretation is guaranteed
  • The JVM process may run with elevated privileges in server or CI environments
  • The impact is complete: arbitrary command execution, data exfiltration, persistence, lateral movement

The Fix

What changed

The fix replaces the single-string Runtime.exec() call with a ProcessBuilder that passes each argument as a discrete token:

Before (vulnerable):

Runtime.getRuntime().exec("cmd.exe start " + applicationFile);

After (safe):

new ProcessBuilder("cmd.exe", "/c", "start", "", applicationFile.getAbsolutePath()).start();

Why this fix works

ProcessBuilder accepts a list of strings where the first element is the executable and each subsequent element is a separate argument. Critically, no shell is invoked to parse this argument list. The arguments are passed directly to the operating system's process creation API (CreateProcess on Windows), which does not interpret shell metacharacters.

This means that even if applicationFile contains & net user hacker /add, the entire string is treated as a single, literal file path argument. The OS will fail to find a file with that name, but it will not execute the injected command. The injection surface is structurally eliminated.

Let's break down the specific arguments in the fix:

Argument Purpose
"cmd.exe" The executable to run
"/c" Tells cmd.exe to execute the following command and then terminate
"start" The Windows start command, which opens a file with its associated program
"" Window title argument required by start when a path follows (prevents path parsing issues)
applicationFile.getAbsolutePath() The fully resolved, absolute path to the file — passed as a discrete token

The use of .getAbsolutePath() is an additional improvement. It resolves the file object to its canonical absolute path before passing it, reducing ambiguity and preventing relative path traversal tricks.

Additional context: the two forms of Runtime.exec()

It is worth noting that Runtime.exec() itself has an overload that accepts a String[] array, which also avoids shell interpretation:

// Also safe — array form bypasses shell parsing
Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", "start", "", applicationFile.getAbsolutePath()});

However, ProcessBuilder is the modern, preferred API because it provides better control over the process environment, working directory, and I/O streams, and its intent is more explicit and readable.


Prevention & Best Practices

1. Never concatenate input into shell command strings

The moment you write "some command " + userInput, you have created a potential injection point. Treat any string concatenation into a command as a red flag that demands immediate review.

2. Always use ProcessBuilder with tokenized arguments

// UNSAFE — string concatenation
Runtime.getRuntime().exec("cmd.exe /c " + userInput);

// SAFE — tokenized arguments, no shell interpretation
new ProcessBuilder("cmd.exe", "/c", userInput).start();

3. Validate file paths with an allowlist

Even with ProcessBuilder, it is good practice to validate that applicationFile resolves to an expected directory:

Path resolved = applicationFile.toPath().toRealPath();
Path allowed = Paths.get("C:\\expected\\app\\directory");
if (!resolved.startsWith(allowed)) {
    throw new SecurityException("File path outside allowed directory: " + resolved);
}

4. Apply the principle of least privilege

Run Java applications with the minimum OS privileges required. If the process does not need to create users or write to system directories, ensure the OS user account it runs under cannot do so either. This limits the blast radius of any successful injection.

5. Use static analysis in your CI pipeline

Tools that can detect this pattern include:

  • Semgrep with the java.lang.security.audit.command-injection-formatted-runtime-exec rule
  • SpotBugs with the FindSecBugs plugin (detects COMMAND_INJECTION)
  • SonarQube (Java security rules for OS command injection)
  • Orbis AppSec (detected and fixed this exact vulnerability automatically)

6. Reference standards


Key Takeaways

  • Runtime.getRuntime().exec(String) with concatenated input is never safe — even when you think the input is controlled, use ProcessBuilder with tokenized arguments instead.
  • cmd.exe interprets metacharacters (&, |, ;, &&, ||) in its arguments, so any code path that invokes cmd.exe with user-influenced data is a command injection risk.
  • The fix in App.java is structurally sound because ProcessBuilder bypasses shell interpretation entirely — it is not a sanitization fix, it is an architectural fix.
  • The empty string "" as the fourth argument to start is a necessary Windows quirk: start treats the first quoted argument as a window title, so an empty title must be passed explicitly to prevent the file path from being misinterpreted.
  • Page object and test framework code runs in CI/CD pipelines where process privileges are often elevated — command injection in test infrastructure can compromise the entire build supply chain.

How Orbis AppSec Detected This

  • Source: The applicationFile variable in App.java, which represents a file path that can be influenced by external configuration or test inputs
  • Sink: Runtime.getRuntime().exec("cmd.exe start " + applicationFile) at page-object/src/main/java/com/iluwatar/pageobject/App.java:80 — a direct OS command execution call with concatenated, unsanitized input
  • Missing control: No allowlist validation, no metacharacter escaping, and no use of a shell-bypass API before the value was passed to exec()
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
  • Fix: Replaced Runtime.getRuntime().exec() with new ProcessBuilder("cmd.exe", "/c", "start", "", applicationFile.getAbsolutePath()).start(), passing arguments as discrete tokens to eliminate shell interpretation

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

Command injection through Runtime.exec() is one of the most dangerous and most preventable vulnerabilities in Java. The pattern is seductive — it looks like you are just running a shell command, and for benign inputs it works exactly as expected. But the moment an attacker controls any part of that concatenated string, the consequences can be catastrophic: full system compromise, data exfiltration, and in CI/CD environments, supply chain attacks.

The fix demonstrated here — replacing Runtime.exec() with a properly tokenized ProcessBuilder call — is not a workaround. It is the architecturally correct approach. By passing arguments as discrete tokens rather than as a shell string, the injection surface is eliminated at the structural level, not patched over with fragile sanitization logic.

If your codebase uses Runtime.exec() with any form of string concatenation, treat it as a critical finding and migrate to ProcessBuilder immediately.


References

Frequently Asked Questions

What is command injection in Java?

Command injection in Java occurs when user-controlled data is embedded into a string that is passed to `Runtime.exec()` or similar APIs as a single shell command. The shell interprets metacharacters like `&`, `|`, and `;` in the input, allowing an attacker to append or replace the intended command with arbitrary OS commands.

How do you prevent command injection in Java?

Use `ProcessBuilder` with a tokenized argument list instead of `Runtime.exec()` with a concatenated string. Each argument should be passed as a separate element in the argument array, which prevents the shell from interpreting metacharacters embedded in the input.

What CWE is command injection?

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

Is input validation enough to prevent command injection in Java?

Input validation helps but is not sufficient on its own. Allowlist validation reduces risk, but the safest approach is to use APIs like `ProcessBuilder` that never pass arguments through a shell interpreter, making metacharacter injection structurally impossible regardless of input content.

Can static analysis detect command injection in Java?

Yes. Static analysis tools such as Semgrep, SpotBugs, and commercial SAST platforms can detect tainted data flowing into `Runtime.exec()` calls. Orbis AppSec's multi-agent AI scanner detected this exact pattern and automatically generated the fix.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #3461

Related Articles

critical

How command injection happens in Go ffmpeg wrappers and how to fix it

A critical command injection vulnerability was discovered in `drivers/local/util.go` where user-influenced file paths were passed directly to `ffmpeg.Input()` without any sanitization. Because many ffmpeg wrapper libraries construct shell command strings under the hood, an attacker could embed shell metacharacters in a file path to execute arbitrary OS commands with server-level privileges. The fix introduces a `sanitizeFilePath()` function that validates paths are absolute, clean, and point to

critical

Critical Shell Injection in autoban.py: How os.system() Opened a Root Shell

A critical shell injection vulnerability in `autoban.py` allowed attackers to execute arbitrary commands as root on OpenWrt routers by crafting malicious connection data containing shell metacharacters. The fix replaces a dangerous `os.system(cmd)` call with `os.fork()` + `os.execvp()`, eliminating shell interpretation entirely. This change ensures that IP addresses extracted from network connections can never be used to inject arbitrary shell commands, even if they contain semicolons, pipes, ba

high

Shell Injection via Unsafe String Concatenation in PaddleOCR Deployment

A high-severity vulnerability was discovered in PaddleOCR's deployment configuration where model download URLs were specified using unencrypted `http://`, exposing users to man-in-the-middle attacks that could allow an attacker to intercept and replace model files with malicious ones. The fix upgrades all model download URLs to use `https://`, ensuring encrypted transmission and integrity of the downloaded files. This change is a critical security baseline for any application that downloads bina

critical

Shell Injection via os.system(): How a Single Line of Code Can Compromise Your System

A critical OS command injection vulnerability (CWE-78) was discovered and patched in `voice.py`, where user-controlled input was interpolated directly into a shell command string passed to `os.system()`. An attacker who could influence the `device` variable — through a config file, environment variable, or any external input — could execute arbitrary system commands with the full privileges of the running process. The fix replaces the dangerous `os.system()` calls with Python's `subprocess.run()

critical

Command Injection via os.system() in DeepSpeed's Data Analyzer: A Critical Fix

A critical command injection vulnerability was discovered in DeepSpeed's `data_analyzer.py`, where an `os.system()` call directly interpolated an unsanitized file path variable into a shell command string. An attacker who could influence dataset configuration or file paths could execute arbitrary shell commands on the host machine. The fix replaces the dangerous shell invocation with safe, Python-native file operations that never touch a shell interpreter.

critical

How unsafe buffer copying happens in C credential storage and how to fix it

A critical vulnerability in `lib/server.c` allowed attackers to trigger out-of-bounds memory reads when copying credentials via unsafe `memcpy()` calls. By replacing `memcpy()` with bounds-safe `strlcpy()`, the fix ensures credentials are safely stored without buffer overruns or null-termination issues.