Skip to content

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.path search 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.
  • URLClassLoader isolates 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 and ProcessBuilder uses 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 ProcessBuilder to prevent LD_PRELOAD attacks
  • 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

Additional Resources