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
dockerfile: Dockerfile
restart: always
ports:
- "8080:8080"
expose:
- "8080"
environment:
MYSQL_HOST: db
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.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;
}
}
}