Ssecurity update
This commit is contained in:
@ -6,8 +6,8 @@ services:
|
||||
context: support-portal-backend
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:8080"
|
||||
expose:
|
||||
- "8080"
|
||||
environment:
|
||||
MYSQL_HOST: db
|
||||
MYSQL_USER: support_portal_user
|
||||
|
||||
@ -11,13 +11,18 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.HashMap;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/files")
|
||||
@CrossOrigin(origins = "*")
|
||||
@CrossOrigin(origins = {
|
||||
"https://cmcbackend.rootxwire.com",
|
||||
"https://maincmc.rootxwire.com",
|
||||
"https://cmctrauma.com"
|
||||
})
|
||||
public class FileController {
|
||||
|
||||
@Value("${file.upload.directory:uploads}")
|
||||
@ -26,48 +31,86 @@ public class FileController {
|
||||
@Value("${app.base-url:http://localhost:8080}")
|
||||
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")
|
||||
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 {
|
||||
// 1. Check empty
|
||||
if (file.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body("File is empty");
|
||||
}
|
||||
|
||||
// Updated validation to accept both images and documents
|
||||
String contentType = file.getContentType();
|
||||
if (!isValidFileType(contentType)) {
|
||||
return ResponseEntity.badRequest().body("Invalid file type. Only images and documents (PDF, DOC, DOCX) are allowed.");
|
||||
// 2. Check file size
|
||||
if (file.getSize() > MAX_FILE_SIZE) {
|
||||
return ResponseEntity.badRequest().body("File size exceeds 5MB limit");
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (!Files.exists(uploadPath)) {
|
||||
Files.createDirectories(uploadPath);
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
|
||||
String uniqueFilename = UUID.randomUUID().toString() + fileExtension;
|
||||
// 8. Generate safe unique filename
|
||||
String uniqueFilename = UUID.randomUUID().toString() + extension.toLowerCase();
|
||||
|
||||
// Save file
|
||||
Path filePath = uploadPath.resolve(uniqueFilename);
|
||||
// 9. Resolve path safely (prevent path traversal)
|
||||
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);
|
||||
|
||||
// FIXED: Return full URL with base URL
|
||||
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(response);
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"url", fileUrl,
|
||||
"filename", uniqueFilename
|
||||
));
|
||||
|
||||
} catch (IOException e) {
|
||||
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}")
|
||||
public ResponseEntity<?> deleteImage(@PathVariable String filename) {
|
||||
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)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
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) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
@ -111,30 +147,67 @@ public class FileController {
|
||||
@GetMapping("/images/{filename}")
|
||||
public ResponseEntity<byte[]> getImage(@PathVariable String filename) {
|
||||
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)) {
|
||||
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);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header("Content-Type", contentType != null ? contentType : "application/octet-stream")
|
||||
.body(imageBytes);
|
||||
.header("Content-Disposition", "inline; filename=\"" + sanitized + "\"")
|
||||
.body(fileBytes);
|
||||
|
||||
} catch (IOException e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidImageType(String contentType) {
|
||||
return contentType != null && (
|
||||
contentType.equals("image/jpeg") ||
|
||||
contentType.equals("image/jpg") ||
|
||||
contentType.equals("image/png") ||
|
||||
contentType.equals("image/gif") ||
|
||||
contentType.equals("image/webp")
|
||||
);
|
||||
// --- Helpers ---
|
||||
|
||||
private String getExtension(String filename) {
|
||||
int dotIndex = filename.lastIndexOf(".");
|
||||
if (dotIndex < 0 || dotIndex == filename.length() - 1) return null;
|
||||
return filename.substring(dotIndex);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user