Description
Allowing external input to select an executable or command string can let an attacker run a different program. Even with a fixed executable, input used as an option or argument may change the program's behavior. This is argument injection. Options that execute another program, such as find's -exec, can turn argument injection into OS command execution.
Java's ProcessBuilder passes the executable and arguments as separate string elements. Without an explicit shell invocation, this avoids shell metacharacter interpretation and preserves argument boundaries, but it does not validate the meaning of each argument. Fix the executable and validate argument semantics too.
Runtime.exec(String) has been deprecated since Java 18. It splits a string on whitespace and does not automatically invoke a shell. Pass the executable and arguments separately instead. The example below supplies the shell, -c and the external command string as separate elements.
Apache Commons Exec 1.6.0's CommandLine.addArguments(String) also parses quotes and spaces. Its API documentation recommends building command lines incrementally because parsing may have unwanted side effects. Individual addArgument calls still do not validate the security meaning of an argument.
Potential impact
- Execution of arbitrary commands or programs with the application's privileges
- Reading or changing sensitive files, credentials or environment data
- Network access, a foothold for privilege escalation or service interruption
The impact depends on the child process's inherited account, filesystem and network permissions.
Remediation
- Prefer a Java library or API that performs the task without starting an external process.
- If a process is necessary, fix the executable path and map user-selectable operations to a finite set of server-owned argument templates.
- Pass the executable and each argument as separate
ProcessBuilderelements. Validate variable options and operands against a strict allow-list for that command and position. A function namedsanitizeorescapeis not enough. - Use
--only if the utility documents it as an option terminator and the input occupies an operand position. It is not a universal sanitization method. - Keep untrusted values out of shell or interpreter code positions such as
sh -c,cmd /c, PowerShell-Commandandpython -c. - Run child processes with the minimum necessary privileges.
Examples
Before
This passes external input directly to a POSIX shell's command-string position, where it is interpreted as shell syntax.
import java.io.IOException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
class CommandController {
@GetMapping("/run")
void run(@RequestParam String command) throws IOException {
new ProcessBuilder("/bin/sh", "-c", command).start();
}
}
Even without a shell, allowing input to choose a program's options can change its behavior.
After
This maps permitted operation identifiers to fixed server-owned argument lists instead of copying input into the command line.
import java.io.IOException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
class OperationController {
@GetMapping("/run")
void run(@RequestParam String operation) throws IOException {
String[] argv = switch (operation) {
case "date" -> new String[] {"/usr/bin/date", "--iso-8601=seconds"};
case "identity" -> new String[] {"/usr/bin/id", "-u"};
default -> throw new IllegalArgumentException("Unsupported operation");
};
new ProcessBuilder(argv).start();
}
}
If input must be passed as a data argument such as a filename or hostname, also validate its format, length and permitted values for the command and argument position.