CWE-114: Process Control - Java
Overview
In Java, CWE-114 vulnerabilities occur when loading native libraries or executing external processes without proper validation. Attackers can exploit weak library loading to inject malicious code via DLL hijacking, path manipulation, or library substitution attacks.
Primary Defence: Use System.load() with absolute paths instead of System.loadLibrary(), configure ProcessBuilder with explicit argument arrays and cleared environments, and implement allowlists for library names and process commands to prevent library substitution and command injection attacks.
Common Vulnerable Patterns
Using System.loadLibrary() with untrusted library name
// VULNERABLE - Searches java.library.path, can be hijacked
public class UnsafeLibraryLoader {
public void loadLibrary(String libraryName) {
// User controls library name - attacker can place malicious library
// in java.library.path or current directory
System.loadLibrary(libraryName);
}
}
// Attack: If attacker controls libraryName or places malicious library
// in search path, their code executes with application privileges
Why this is vulnerable: System.loadLibrary() searches java.library.path and other system paths, allowing attackers who can place malicious libraries in these directories or manipulate the library path to execute arbitrary native code.
Loading library from user-controlled path
// VULNERABLE - Path can be manipulated
public class UnsafeNativeLoader {
public void loadNativeLib(String userPath) {
// User controls full path - can point to malicious library
System.load(userPath);
}
}
// Attack: User provides "/tmp/evil.so" containing malicious code
Why this is vulnerable: Accepting user-controlled paths for System.load() allows attackers to load arbitrary native libraries from any location, including attacker-controlled directories, leading to arbitrary code execution.
Building library path from user input
// VULNERABLE - Path traversal + library injection
public class UnsafePluginLoader {
private static final String PLUGIN_DIR = "/opt/app/plugins/";
public void loadPlugin(String pluginName) {
// User controls pluginName - can use path traversal
String libraryPath = PLUGIN_DIR + pluginName + ".so";
System.load(libraryPath);
}
}
// Attack: pluginName = "../../../tmp/malicious"
// Loads: /opt/app/plugins/../../../tmp/malicious.so = /tmp/malicious.so
Why this is vulnerable: Concatenating user input into file paths without validation allows path traversal attacks using ../ sequences to escape the intended directory and load malicious libraries from arbitrary locations.
Using Runtime.exec() with unsanitized input
// VULNERABLE - Command injection
public class UnsafeProcessLauncher {
public void convertImage(String inputFile) {
try {
// User controls inputFile - can inject commands
String command = "convert " + inputFile + " output.png";
Runtime.getRuntime().exec(command);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// Attack: inputFile = "input.jpg; rm -rf /"
// Executes: convert input.jpg; rm -rf / output.png
Why this is vulnerable: Passing a single string to Runtime.exec() invokes the shell, allowing command injection via shell metacharacters (;, |, &, etc.) that can execute arbitrary commands.
Secure Patterns
Use System.load() with absolute path and validation
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;
public class SecureLibraryLoader {
// Hardcoded allowlist of allowed libraries
private static final Path LIBRARY_DIR = Paths.get("/opt/app/lib").toAbsolutePath();
private static final Set<String> ALLOWED_LIBRARIES = Set.of(
"libcrypto.so",
"libssl.so",
"libcustom.so"
);
public void loadLibrary(String libraryName) {
// Validate library is in allowlist
if (!ALLOWED_LIBRARIES.contains(libraryName)) {
throw new SecurityException("Library not in allowlist: " + libraryName);
}
// Construct absolute path
Path libraryPath = LIBRARY_DIR.resolve(libraryName).normalize();
// Verify path hasn't escaped library directory (path traversal protection)
if (!libraryPath.startsWith(LIBRARY_DIR)) {
throw new SecurityException("Path traversal attempt detected");
}
// Verify file exists and is a regular file
File libraryFile = libraryPath.toFile();
if (!libraryFile.exists() || !libraryFile.isFile()) {
throw new SecurityException("Library file not found or is not a file");
}
// Load with absolute path - bypasses library search path
System.load(libraryPath.toString());
}
}
Why this works:
- Absolute paths bypass
java.library.pathsearch behavior. - Allowlists restrict loading to approved library names.
normalize()resolves.and..to block traversal.startsWith()prevents escaping the library directory.- File checks ensure only regular files are loaded.
Secure ProcessBuilder with argument array
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;
public class SecureProcessLauncher {
private static final Path ALLOWED_BINARY_DIR = Paths.get("/usr/bin").toAbsolutePath();
private static final Set<String> ALLOWED_COMMANDS = Set.of("convert", "ffmpeg", "gs");
public void convertImage(String inputFile, String outputFile) throws IOException {
// Validate input file path
Path inputPath = validateFilePath(inputFile);
Path outputPath = validateFilePath(outputFile);
// Use ProcessBuilder with argument array (no shell interpretation)
ProcessBuilder pb = new ProcessBuilder(
"/usr/bin/convert", // Absolute path to binary
inputPath.toString(),
outputPath.toString()
);
// Disable environment variable inheritance to prevent LD_PRELOAD attacks
pb.environment().clear();
// Set safe working directory
pb.directory(new File("/tmp/safe-workspace"));
// Execute process
Process process = pb.start();
try {
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("Process failed with exit code: " + exitCode);
}
} catch (InterruptedException e) {
process.destroy();
Thread.currentThread().interrupt();
throw new IOException("Process interrupted", e);
}
}
private Path validateFilePath(String filePath) {
Path path = Paths.get(filePath).toAbsolutePath().normalize();
// Ensure path is within allowed directory
Path allowedDir = Paths.get("/opt/app/uploads").toAbsolutePath();
if (!path.startsWith(allowedDir)) {
throw new SecurityException("Path outside allowed directory");
}
// Ensure file exists and is regular file
if (!Files.isRegularFile(path)) {
throw new SecurityException("Not a regular file");
}
return path;
}
}
Why this works:
- Argument arrays avoid shell interpretation and metacharacter injection.
- Absolute binary paths prevent PATH hijacking.
- Clearing the environment removes
LD_PRELOAD/similar abuse. - Canonical path checks enforce allowed directories.
Secure JNI library loading with signature verification
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
public class SecureJNILoader {
private static final Path LIBRARY_DIR = Paths.get("/opt/app/lib/native").toAbsolutePath();
// SHA-256 hashes of trusted libraries
private static final Map<String, String> LIBRARY_HASHES = Map.of(
"libcrypto.so", "abc123...def456",
"libssl.so", "789xyz...012abc"
);
public void loadTrustedLibrary(String libraryName) {
if (!LIBRARY_HASHES.containsKey(libraryName)) {
throw new SecurityException("Library not in trusted list: " + libraryName);
}
Path libraryPath = LIBRARY_DIR.resolve(libraryName).normalize();
// Verify path integrity
if (!libraryPath.startsWith(LIBRARY_DIR)) {
throw new SecurityException("Path traversal detected");
}
// Verify library hash before loading
String expectedHash = LIBRARY_HASHES.get(libraryName);
String actualHash = calculateSHA256(libraryPath);
if (!expectedHash.equals(actualHash)) {
throw new SecurityException("Library hash mismatch - possible tampering");
}
// Load verified library
System.load(libraryPath.toString());
}
private String calculateSHA256(Path filePath) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] fileBytes = Files.readAllBytes(filePath);
byte[] hashBytes = digest.digest(fileBytes);
// Convert to hex string
StringBuilder sb = new StringBuilder();
for (byte b : hashBytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException | IOException e) {
throw new SecurityException("Failed to verify library hash", e);
}
}
}
Why this works:
- SHA-256 verification detects tampering or substitution.
- Allowlists restrict which libraries can be loaded.
- Path normalization blocks traversal and hijacking.
- Library must match name, path, and hash to load.
Plugin system with secure class loading
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class SecurePluginLoader {
private static final Path PLUGIN_DIR = Paths.get("/opt/app/plugins").toAbsolutePath();
public Object loadPlugin(String pluginName, Class<?> pluginInterface) {
// Validate plugin name (alphanumeric only)
if (!pluginName.matches("^[a-zA-Z0-9_-]+$")) {
throw new SecurityException("Invalid plugin name");
}
Path pluginPath = PLUGIN_DIR.resolve(pluginName + ".jar").normalize();
// Verify path integrity
if (!pluginPath.startsWith(PLUGIN_DIR)) {
throw new SecurityException("Path traversal attempt");
}
if (!Files.isRegularFile(pluginPath)) {
throw new SecurityException("Plugin file not found");
}
try {
// Create restricted classloader
URL pluginUrl = pluginPath.toUri().toURL();
URLClassLoader classLoader = new URLClassLoader(
new URL[]{pluginUrl},
this.getClass().getClassLoader()
);
// Load plugin class
String className = "com.app.plugins." + pluginName + ".Plugin";
Class<?> pluginClass = classLoader.loadClass(className);
// Verify plugin implements required interface
if (!pluginInterface.isAssignableFrom(pluginClass)) {
throw new SecurityException("Plugin doesn't implement required interface");
}
// Instantiate plugin
return pluginClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new SecurityException("Failed to load plugin", e);
}
}
}
Why this works:
- Name validation enforces an allowlist-safe format.
- Path normalization blocks traversal outside plugin directory.
URLClassLoaderisolates plugin loading context.- Interface checks ensure the expected contract is implemented.
- Only validated plugins from trusted paths are instantiated.
Verification
After implementing secure library loading with absolute paths and process isolation, verify the fix through multiple approaches:
- Manual testing: Attempt to load unauthorized libraries or execute malicious commands and verify they're rejected
- Code review: Confirm all
System.load()calls use absolute paths andProcessBuilderuses argument arrays with no shell interpretation - Static analysis: Use security scanners to verify no process control vulnerabilities exist
- Regression testing: Ensure legitimate library loading and subprocess execution continue to work correctly
- Edge case validation: Test with path traversal (
../), command injection (;,|,&&), and verify proper validation - Environment verification: Confirm environment variables are cleared in
ProcessBuilderto preventLD_PRELOADattacks - Allowlist validation: Verify only approved libraries and binaries can be loaded/executed
- Hash verification: If implemented, confirm cryptographic signatures are validated before loading JNI libraries
- Rescan: Run the security scanner again to confirm the finding is resolved