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());
}
}