Ssecurity update

This commit is contained in:
2026-02-23 09:43:24 +05:30
parent 152ea94034
commit b97e57e070
2 changed files with 130 additions and 57 deletions

View File

@ -6,8 +6,8 @@ services:
context: support-portal-backend context: support-portal-backend
dockerfile: Dockerfile dockerfile: Dockerfile
restart: always restart: always
ports: expose:
- "8080:8080" - "8080"
environment: environment:
MYSQL_HOST: db MYSQL_HOST: db
MYSQL_USER: support_portal_user MYSQL_USER: support_portal_user

View File

@ -11,13 +11,18 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.HashMap; import java.util.Arrays;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@RequestMapping("/api/files") @RequestMapping("/api/files")
@CrossOrigin(origins = "*") @CrossOrigin(origins = {
"https://cmcbackend.rootxwire.com",
"https://maincmc.rootxwire.com",
"https://cmctrauma.com"
})
public class FileController { public class FileController {
@Value("${file.upload.directory:uploads}") @Value("${file.upload.directory:uploads}")
@ -26,48 +31,86 @@ public class FileController {
@Value("${app.base-url:http://localhost:8080}") @Value("${app.base-url:http://localhost:8080}")
private String baseUrl; private String baseUrl;
private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList(
".jpg", ".jpeg", ".png", ".gif", ".webp",
".pdf", ".doc", ".docx"
);
private static final List<String> ALLOWED_CONTENT_TYPES = Arrays.asList(
"image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp",
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
);
private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
@PostMapping("/upload") @PostMapping("/upload")
public ResponseEntity<?> uploadFile(@RequestParam("file") MultipartFile file) { public ResponseEntity<?> uploadFile(@RequestParam("file") MultipartFile file) {
System.out.println("=== FILE UPLOAD DEBUG ===");
System.out.println("File name: " + file.getOriginalFilename());
System.out.println("Content type: " + file.getContentType());
System.out.println("Base URL: " + baseUrl);
try { try {
// 1. Check empty
if (file.isEmpty()) { if (file.isEmpty()) {
return ResponseEntity.badRequest().body("File is empty"); return ResponseEntity.badRequest().body("File is empty");
} }
// Updated validation to accept both images and documents // 2. Check file size
String contentType = file.getContentType(); if (file.getSize() > MAX_FILE_SIZE) {
if (!isValidFileType(contentType)) { return ResponseEntity.badRequest().body("File size exceeds 5MB limit");
return ResponseEntity.badRequest().body("Invalid file type. Only images and documents (PDF, DOC, DOCX) are allowed.");
} }
// Create upload directory if it doesn't exist // 3. Get and sanitize original filename
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || originalFilename.isBlank()) {
return ResponseEntity.badRequest().body("Invalid filename");
}
// Prevent path traversal - only take the actual filename
String sanitizedFilename = Paths.get(originalFilename).getFileName().toString();
// 4. Validate extension (cannot be spoofed by client unlike Content-Type)
String extension = getExtension(sanitizedFilename);
if (extension == null || !ALLOWED_EXTENSIONS.contains(extension.toLowerCase())) {
return ResponseEntity.badRequest()
.body("Invalid file type. Allowed: JPG, PNG, GIF, WEBP, PDF, DOC, DOCX");
}
// 5. Validate Content-Type (secondary check)
String contentType = file.getContentType();
if (!ALLOWED_CONTENT_TYPES.contains(contentType)) {
return ResponseEntity.badRequest()
.body("Invalid content type.");
}
// 6. Validate Content-Type matches extension
if (!isContentTypeMatchingExtension(contentType, extension)) {
return ResponseEntity.badRequest()
.body("File type mismatch detected.");
}
// 7. Create upload directory if needed
Path uploadPath = Paths.get(uploadDirectory); Path uploadPath = Paths.get(uploadDirectory);
if (!Files.exists(uploadPath)) { if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath); Files.createDirectories(uploadPath);
} }
// Generate unique filename // 8. Generate safe unique filename
String originalFilename = file.getOriginalFilename(); String uniqueFilename = UUID.randomUUID().toString() + extension.toLowerCase();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
String uniqueFilename = UUID.randomUUID().toString() + fileExtension;
// Save file // 9. Resolve path safely (prevent path traversal)
Path filePath = uploadPath.resolve(uniqueFilename); Path filePath = uploadPath.resolve(uniqueFilename).normalize();
if (!filePath.startsWith(uploadPath.normalize())) {
return ResponseEntity.badRequest().body("Invalid file path");
}
// 10. Save file
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
// FIXED: Return full URL with base URL
String fileUrl = baseUrl + "/uploads/" + uniqueFilename; String fileUrl = baseUrl + "/uploads/" + uniqueFilename;
Map<String, String> response = new HashMap<>();
response.put("url", fileUrl);
response.put("filename", uniqueFilename);
System.out.println("File uploaded successfully. URL: " + fileUrl); return ResponseEntity.ok(Map.of(
"url", fileUrl,
return ResponseEntity.ok(response); "filename", uniqueFilename
));
} catch (IOException e) { } catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
@ -75,32 +118,25 @@ public class FileController {
} }
} }
// Updated validation method
private boolean isValidFileType(String contentType) {
return contentType != null && (
// Image types
contentType.equals("image/jpeg") ||
contentType.equals("image/jpg") ||
contentType.equals("image/png") ||
contentType.equals("image/gif") ||
contentType.equals("image/webp") ||
// Document types for resumes
contentType.equals("application/pdf") ||
contentType.equals("application/msword") ||
contentType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document")
);
}
@DeleteMapping("/images/{filename}") @DeleteMapping("/images/{filename}")
public ResponseEntity<?> deleteImage(@PathVariable String filename) { public ResponseEntity<?> deleteImage(@PathVariable String filename) {
try { try {
Path filePath = Paths.get(uploadDirectory).resolve(filename); // Prevent path traversal
String sanitized = Paths.get(filename).getFileName().toString();
Path uploadPath = Paths.get(uploadDirectory).normalize();
Path filePath = uploadPath.resolve(sanitized).normalize();
if (!filePath.startsWith(uploadPath)) {
return ResponseEntity.badRequest().body("Invalid filename");
}
if (!Files.exists(filePath)) { if (!Files.exists(filePath)) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
Files.delete(filePath); Files.delete(filePath);
return ResponseEntity.ok().body(Map.of("message", "File deleted successfully")); return ResponseEntity.ok(Map.of("message", "File deleted successfully"));
} catch (IOException e) { } catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
@ -111,30 +147,67 @@ public class FileController {
@GetMapping("/images/{filename}") @GetMapping("/images/{filename}")
public ResponseEntity<byte[]> getImage(@PathVariable String filename) { public ResponseEntity<byte[]> getImage(@PathVariable String filename) {
try { try {
Path filePath = Paths.get(uploadDirectory).resolve(filename); // Prevent path traversal
String sanitized = Paths.get(filename).getFileName().toString();
Path uploadPath = Paths.get(uploadDirectory).normalize();
Path filePath = uploadPath.resolve(sanitized).normalize();
if (!filePath.startsWith(uploadPath)) {
return ResponseEntity.badRequest().build();
}
if (!Files.exists(filePath)) { if (!Files.exists(filePath)) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
byte[] imageBytes = Files.readAllBytes(filePath); // Validate it's an allowed file type before serving
String ext = getExtension(sanitized);
if (ext == null || !ALLOWED_EXTENSIONS.contains(ext.toLowerCase())) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
byte[] fileBytes = Files.readAllBytes(filePath);
String contentType = Files.probeContentType(filePath); String contentType = Files.probeContentType(filePath);
return ResponseEntity.ok() return ResponseEntity.ok()
.header("Content-Type", contentType != null ? contentType : "application/octet-stream") .header("Content-Type", contentType != null ? contentType : "application/octet-stream")
.body(imageBytes); .header("Content-Disposition", "inline; filename=\"" + sanitized + "\"")
.body(fileBytes);
} catch (IOException e) { } catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
} }
} }
private boolean isValidImageType(String contentType) { // --- Helpers ---
return contentType != null && (
contentType.equals("image/jpeg") || private String getExtension(String filename) {
contentType.equals("image/jpg") || int dotIndex = filename.lastIndexOf(".");
contentType.equals("image/png") || if (dotIndex < 0 || dotIndex == filename.length() - 1) return null;
contentType.equals("image/gif") || return filename.substring(dotIndex);
contentType.equals("image/webp") }
);
private boolean isContentTypeMatchingExtension(String contentType, String extension) {
String ext = extension.toLowerCase();
switch (ext) {
case ".jpg":
case ".jpeg":
return contentType.equals("image/jpeg") || contentType.equals("image/jpg");
case ".png":
return contentType.equals("image/png");
case ".gif":
return contentType.equals("image/gif");
case ".webp":
return contentType.equals("image/webp");
case ".pdf":
return contentType.equals("application/pdf");
case ".doc":
return contentType.equals("application/msword");
case ".docx":
return contentType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
default:
return false;
}
} }
} }