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
•Technically reviewed by Anupam Mediratta•Published June 6, 2026•Reviewed 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

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.


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.


Prevention and further reading

Related Articles

critical

webapp-testing Server Script: Shell Injection via Unquoted Command

A critical command injection vulnerability in a web server testing utility allowed attackers to execute arbitrary shell commands by injecting metacharacters into command-line arguments. The fix removes shell interpretation and safely tokenizes user-supplied commands using `shlex.split()`, while preserving support for legitimate directory-change workflows.

high

THiNX Device Platform: Command Injection via source.owner in Git Fetch

The THiNX device management platform fixed a critical vulnerability where the `source.owner` parameter flowed directly into shell commands during git repository operations. An attacker with source configuration access could inject arbitrary shell commands by providing malicious owner values containing metacharacters like `; curl`.

critical

backtest.js CLI args: Unvalidated --days and --start Options

The backtest.js command-line tool accepted --days and --start arguments without validation, creating a command injection vector when the script is invoked by upstream processes with untrusted input. The fix adds strict input validation: --days must be a positive integer ≤ 3650, and --start must match the YYYY-MM-DD date format.

critical

Tauri type_text() Command Injection via Control Characters in

The type_text() system command handler in Tauri accepted arbitrary text up to 2000 UTF-16 characters without validating content, allowing injection of control characters that SendInput's Unicode path interprets as real key presses. An attacker could inject newline, escape, or tab characters to trigger actions in the focused window beyond mere text typing.

high

runStreaming() Command Injection: Defense-in-Depth for Electron Child

An Electron application's `runStreaming()` utility accepted a command string and argument array without validating either, creating a latent command injection vector. The fix adds strict type checking and a whitelist regex that rejects shell metacharacters, bounding the failure mode even if caller input becomes attacker-influenced.

critical

docker_rpc.uc Command Injection: Unsanitized RPC Parameters

A critical command injection vulnerability in the Docker RPC handler allowed authenticated attackers to execute arbitrary system commands by injecting shell metacharacters into container ID, port, user ID, or command parameters. The fix validates all user-supplied inputs against strict whitelist patterns before interpolating them into shell commands.