import org.springframework.web.bind.annotation.*;
import org.springframework.core.io.Resource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpHeaders;
import java.nio.file.*;
import java.io.IOException;
import java.util.Optional;
import java.util.Set;
@RestController
public class SecureFileController {
private static final Path BASE_DIR = Paths.get("/var/app/files").toAbsolutePath().normalize();
private static final Set<String> ALLOWED_EXTENSIONS = Set.of(
".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt", ".docx"
);
@Autowired
private SecureFilePathUtil filePathUtil;
// Secure: Proper path validation and normalization
@GetMapping("/download/{filename}")
public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
// Validate filename
if (!filePathUtil.isValidFilename(filename)) {
return ResponseEntity.badRequest().build();
}
// Check allowed extensions
String extension = getFileExtension(filename).toLowerCase();
if (!ALLOWED_EXTENSIONS.contains(extension)) {
return ResponseEntity.badRequest().build();
}
// Securely resolve path
Optional<Path> securePathOpt = filePathUtil.resolveSecurePath(BASE_DIR, filename);
if (securePathOpt.isEmpty()) {
return ResponseEntity.badRequest().build();
}
Path securePath = securePathOpt.get();
try {
if (!Files.exists(securePath) || !Files.isRegularFile(securePath)) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(securePath);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"")
.body(resource);
} catch (Exception e) {
logger.error("File access error: {}", filename, e);
return ResponseEntity.badRequest().build();
}
}
// Secure: Category validation with allowlist
@GetMapping("/files/{category}/{filename}")
public ResponseEntity<String> readFile(
@PathVariable String category,
@PathVariable String filename) {
// Validate category against allowlist
Set<String> allowedCategories = Set.of("documents", "images", "reports");
if (!allowedCategories.contains(category)) {
return ResponseEntity.badRequest().build();
}
// Validate filename
if (!filePathUtil.isValidFilename(filename)) {
return ResponseEntity.badRequest().build();
}
try {
// Securely resolve category directory
Optional<Path> categoryPathOpt = filePathUtil.resolveSecurePath(BASE_DIR, category);
if (categoryPathOpt.isEmpty()) {
return ResponseEntity.badRequest().build();
}
// Securely resolve file within category
Optional<Path> filePathOpt = filePathUtil.resolveSecurePath(categoryPathOpt.get(), filename);
if (filePathOpt.isEmpty()) {
return ResponseEntity.badRequest().build();
}
Path filePath = filePathOpt.get();
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
return ResponseEntity.notFound().build();
}
// Check file size limit
if (Files.size(filePath) > 1024 * 1024) { // 1MB limit
return ResponseEntity.badRequest().body("File too large");
}
String content = Files.readString(filePath);
return ResponseEntity.ok(content);
} catch (IOException e) {
logger.error("File read error: {}/{}", category, filename, e);
return ResponseEntity.internalServerError().build();
}
}
// Secure: Multiple layers of validation
@PostMapping("/upload")
public ResponseEntity<String> uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam("category") String category) {
// Validate file
if (file.isEmpty() || file.getSize() > 10 * 1024 * 1024) { // 10MB limit
return ResponseEntity.badRequest().body("Invalid file size");
}
String originalFilename = file.getOriginalFilename();
if (!filePathUtil.isValidFilename(originalFilename)) {
return ResponseEntity.badRequest().body("Invalid filename");
}
// Validate category
Set<String> allowedCategories = Set.of("documents", "images", "uploads");
if (!allowedCategories.contains(category)) {
return ResponseEntity.badRequest().body("Invalid category");
}
// Check file extension
String extension = getFileExtension(originalFilename).toLowerCase();
if (!ALLOWED_EXTENSIONS.contains(extension)) {
return ResponseEntity.badRequest().body("File type not allowed");
}
try {
// Securely resolve upload directory
Optional<Path> categoryPathOpt = filePathUtil.resolveSecurePath(BASE_DIR, category);
if (categoryPathOpt.isEmpty()) {
return ResponseEntity.badRequest().body("Invalid category path");
}
Path categoryPath = categoryPathOpt.get();
Files.createDirectories(categoryPath);
// Generate unique filename to prevent conflicts
String uniqueFilename = generateUniqueFilename(originalFilename);
// Securely resolve target file path
Optional<Path> targetPathOpt = filePathUtil.resolveSecurePath(categoryPath, uniqueFilename);
if (targetPathOpt.isEmpty()) {
return ResponseEntity.badRequest().body("Invalid target path");
}
Path targetPath = targetPathOpt.get();
// Copy file with atomic operation
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.ATOMIC_MOVE);
// Set secure file permissions
Files.setPosixFilePermissions(targetPath,
PosixFilePermissions.fromString("rw-r--r--"));
return ResponseEntity.ok("File uploaded: " + uniqueFilename);
} catch (IOException e) {
logger.error("Upload error: {}", originalFilename, e);
return ResponseEntity.internalServerError().body("Upload failed");
}
}
// Secure: User isolation with proper validation
@GetMapping("/user-files/{userId}/{document}")
public ResponseEntity<Resource> getUserDocument(
@PathVariable String userId,
@PathVariable String document,
Authentication auth) {
// Authorization: Users can only access their own files
if (!userId.equals(auth.getName()) && !hasAdminRole(auth)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
// Validate user ID format (e.g., UUID or alphanumeric)
if (!userId.matches("[a-zA-Z0-9-]{1,36}")) {
return ResponseEntity.badRequest().build();
}
// Validate document name
if (!filePathUtil.isValidFilename(document)) {
return ResponseEntity.badRequest().build();
}
try {
// Securely resolve user directory
Optional<Path> userDirOpt = filePathUtil.resolveSecurePath(BASE_DIR, "users/" + userId);
if (userDirOpt.isEmpty()) {
return ResponseEntity.badRequest().build();
}
// Securely resolve document path
Optional<Path> documentPathOpt = filePathUtil.resolveSecurePath(userDirOpt.get(), document);
if (documentPathOpt.isEmpty()) {
return ResponseEntity.badRequest().build();
}
Path documentPath = documentPathOpt.get();
if (!Files.exists(documentPath) || !Files.isRegularFile(documentPath)) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(documentPath);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + document + "\"")
.body(resource);
} catch (Exception e) {
logger.error("User document access error: {}/{}", userId, document, e);
return ResponseEntity.badRequest().build();
}
}
private String getFileExtension(String filename) {
int lastDot = filename.lastIndexOf('.');
return lastDot > 0 ? filename.substring(lastDot) : "";
}
private String generateUniqueFilename(String originalFilename) {
String baseName = originalFilename;
String extension = "";
int lastDot = originalFilename.lastIndexOf('.');
if (lastDot > 0) {
baseName = originalFilename.substring(0, lastDot);
extension = originalFilename.substring(lastDot);
}
String timestamp = String.valueOf(System.currentTimeMillis());
String randomId = UUID.randomUUID().toString().substring(0, 8);
return baseName + "_" + timestamp + "_" + randomId + extension;
}
private boolean hasAdminRole(Authentication auth) {
return auth.getAuthorities().stream()
.anyMatch(grantedAuthority -> "ROLE_ADMIN".equals(grantedAuthority.getAuthority()));
}
}