fix(back-end) security issue
Some checks failed
Build, Test, Deploy and Analysis / test-backend (pull_request) Successful in 39s
Build, Test, Deploy and Analysis / build-and-push (push) Failing after 34s
Build, Test, Deploy and Analysis / deploy (pull_request) Successful in 8s
Build, Test, Deploy and Analysis / qodana (push) Failing after 8s
Build, Test, Deploy and Analysis / qodana (pull_request) Failing after 11s
Build, Test, Deploy and Analysis / test-backend (push) Successful in 40s
Build, Test, Deploy and Analysis / build-and-push (pull_request) Successful in 20s
Build, Test, Deploy and Analysis / deploy (push) Has been skipped
Some checks failed
Build, Test, Deploy and Analysis / test-backend (pull_request) Successful in 39s
Build, Test, Deploy and Analysis / build-and-push (push) Failing after 34s
Build, Test, Deploy and Analysis / deploy (pull_request) Successful in 8s
Build, Test, Deploy and Analysis / qodana (push) Failing after 8s
Build, Test, Deploy and Analysis / qodana (pull_request) Failing after 11s
Build, Test, Deploy and Analysis / test-backend (push) Successful in 40s
Build, Test, Deploy and Analysis / build-and-push (pull_request) Successful in 20s
Build, Test, Deploy and Analysis / deploy (push) Has been skipped
This commit is contained in:
@@ -28,7 +28,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
# In Gitea, pr-mode funziona se il runner ha accesso ai dati del clone
|
# In Gitea, pr-mode funziona se il runner ha accesso ai dati del clone
|
||||||
pr-mode: ${{ gitea.event_name == 'pull_request' }}
|
pr-mode: ${{ gitea.event_name == 'pull_request' }}
|
||||||
use-caches: true
|
use-caches: false
|
||||||
# Nota: Gitea ha un supporto limitato per i commenti automatici
|
# Nota: Gitea ha un supporto limitato per i commenti automatici
|
||||||
# rispetto a GitHub, ma l'analisi verrà eseguita correttamente.
|
# rispetto a GitHub, ma l'analisi verrà eseguita correttamente.
|
||||||
post-pr-comment: false
|
post-pr-comment: false
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.printcalculator.repository.CustomQuoteRequestRepository;
|
|||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
@@ -14,13 +15,18 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.InvalidPathException;
|
||||||
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.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/custom-quote-requests")
|
@RequestMapping("/api/custom-quote-requests")
|
||||||
@@ -31,7 +37,8 @@ public class CustomQuoteRequestController {
|
|||||||
private final com.printcalculator.service.ClamAVService clamAVService;
|
private final com.printcalculator.service.ClamAVService clamAVService;
|
||||||
|
|
||||||
// TODO: Inject Storage Service
|
// TODO: Inject Storage Service
|
||||||
private static final String STORAGE_ROOT = "storage_requests";
|
private static final Path STORAGE_ROOT = Paths.get("storage_requests").toAbsolutePath().normalize();
|
||||||
|
private static final Pattern SAFE_EXTENSION_PATTERN = Pattern.compile("^[a-z0-9]{1,10}$");
|
||||||
private static final Set<String> FORBIDDEN_COMPRESSED_EXTENSIONS = Set.of(
|
private static final Set<String> FORBIDDEN_COMPRESSED_EXTENSIONS = Set.of(
|
||||||
"zip", "rar", "7z", "tar", "gz", "tgz", "bz2", "tbz2", "xz", "txz", "zst"
|
"zip", "rar", "7z", "tar", "gz", "tgz", "bz2", "tbz2", "xz", "txz", "zst"
|
||||||
);
|
);
|
||||||
@@ -116,8 +123,7 @@ public class CustomQuoteRequestController {
|
|||||||
|
|
||||||
// Generate path
|
// Generate path
|
||||||
UUID fileUuid = UUID.randomUUID();
|
UUID fileUuid = UUID.randomUUID();
|
||||||
String ext = getExtension(file.getOriginalFilename());
|
String storedFilename = fileUuid + ".upload";
|
||||||
String storedFilename = fileUuid.toString() + "." + ext;
|
|
||||||
|
|
||||||
// Note: We don't have attachment ID yet.
|
// Note: We don't have attachment ID yet.
|
||||||
// We'll save attachment first to get ID.
|
// We'll save attachment first to get ID.
|
||||||
@@ -126,14 +132,22 @@ public class CustomQuoteRequestController {
|
|||||||
|
|
||||||
attachment = attachmentRepo.save(attachment);
|
attachment = attachmentRepo.save(attachment);
|
||||||
|
|
||||||
String relativePath = "quote-requests/" + request.getId() + "/attachments/" + attachment.getId() + "/" + storedFilename;
|
Path relativePath = Path.of(
|
||||||
attachment.setStoredRelativePath(relativePath);
|
"quote-requests",
|
||||||
|
request.getId().toString(),
|
||||||
|
"attachments",
|
||||||
|
attachment.getId().toString(),
|
||||||
|
storedFilename
|
||||||
|
);
|
||||||
|
attachment.setStoredRelativePath(relativePath.toString());
|
||||||
attachmentRepo.save(attachment);
|
attachmentRepo.save(attachment);
|
||||||
|
|
||||||
// Save file to disk
|
// Save file to disk
|
||||||
Path absolutePath = Paths.get(STORAGE_ROOT, relativePath);
|
Path absolutePath = resolveWithinStorageRoot(relativePath);
|
||||||
Files.createDirectories(absolutePath.getParent());
|
Files.createDirectories(absolutePath.getParent());
|
||||||
Files.copy(file.getInputStream(), absolutePath);
|
try (InputStream inputStream = file.getInputStream()) {
|
||||||
|
Files.copy(inputStream, absolutePath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,9 +165,16 @@ public class CustomQuoteRequestController {
|
|||||||
// Helper
|
// Helper
|
||||||
private String getExtension(String filename) {
|
private String getExtension(String filename) {
|
||||||
if (filename == null) return "dat";
|
if (filename == null) return "dat";
|
||||||
int i = filename.lastIndexOf('.');
|
String cleaned = StringUtils.cleanPath(filename);
|
||||||
if (i > 0) {
|
if (cleaned.contains("..")) {
|
||||||
return filename.substring(i + 1).toLowerCase();
|
return "dat";
|
||||||
|
}
|
||||||
|
int i = cleaned.lastIndexOf('.');
|
||||||
|
if (i > 0 && i < cleaned.length() - 1) {
|
||||||
|
String ext = cleaned.substring(i + 1).toLowerCase(Locale.ROOT);
|
||||||
|
if (SAFE_EXTENSION_PATTERN.matcher(ext).matches()) {
|
||||||
|
return ext;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return "dat";
|
return "dat";
|
||||||
}
|
}
|
||||||
@@ -166,4 +187,20 @@ public class CustomQuoteRequestController {
|
|||||||
String mime = file.getContentType();
|
String mime = file.getContentType();
|
||||||
return mime != null && FORBIDDEN_COMPRESSED_MIME_TYPES.contains(mime.toLowerCase());
|
return mime != null && FORBIDDEN_COMPRESSED_MIME_TYPES.contains(mime.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Path resolveWithinStorageRoot(Path relativePath) {
|
||||||
|
try {
|
||||||
|
Path normalizedRelative = relativePath.normalize();
|
||||||
|
if (normalizedRelative.isAbsolute()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid attachment path");
|
||||||
|
}
|
||||||
|
Path absolutePath = STORAGE_ROOT.resolve(normalizedRelative).normalize();
|
||||||
|
if (!absolutePath.startsWith(STORAGE_ROOT)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid attachment path");
|
||||||
|
}
|
||||||
|
return absolutePath;
|
||||||
|
} catch (InvalidPathException e) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid attachment path");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,18 +11,17 @@ import com.printcalculator.service.StorageService;
|
|||||||
import com.printcalculator.service.TwintPaymentService;
|
import com.printcalculator.service.TwintPaymentService;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.InvalidPathException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -30,10 +29,13 @@ import java.util.HashMap;
|
|||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/orders")
|
@RequestMapping("/api/orders")
|
||||||
public class OrderController {
|
public class OrderController {
|
||||||
|
private static final Pattern SAFE_EXTENSION_PATTERN = Pattern.compile("^[a-z0-9]{1,10}$");
|
||||||
|
|
||||||
private final OrderService orderService;
|
private final OrderService orderService;
|
||||||
private final OrderRepository orderRepo;
|
private final OrderRepository orderRepo;
|
||||||
@@ -104,15 +106,21 @@ public class OrderController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String relativePath = item.getStoredRelativePath();
|
String relativePath = item.getStoredRelativePath();
|
||||||
|
Path destinationRelativePath;
|
||||||
if (relativePath == null || relativePath.equals("PENDING")) {
|
if (relativePath == null || relativePath.equals("PENDING")) {
|
||||||
String ext = getExtension(file.getOriginalFilename());
|
String ext = getExtension(file.getOriginalFilename());
|
||||||
String storedFilename = UUID.randomUUID().toString() + "." + ext;
|
String storedFilename = UUID.randomUUID() + "." + ext;
|
||||||
relativePath = "orders/" + orderId + "/3d-files/" + orderItemId + "/" + storedFilename;
|
destinationRelativePath = Path.of("orders", orderId.toString(), "3d-files", orderItemId.toString(), storedFilename);
|
||||||
item.setStoredRelativePath(relativePath);
|
item.setStoredRelativePath(destinationRelativePath.toString());
|
||||||
item.setStoredFilename(storedFilename);
|
item.setStoredFilename(storedFilename);
|
||||||
|
} else {
|
||||||
|
destinationRelativePath = resolveOrderItemRelativePath(relativePath, orderId, orderItemId);
|
||||||
|
if (destinationRelativePath == null) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
storageService.store(file, Paths.get(relativePath));
|
storageService.store(file, destinationRelativePath);
|
||||||
item.setFileSizeBytes(file.getSize());
|
item.setFileSizeBytes(file.getSize());
|
||||||
item.setMimeType(file.getContentType());
|
item.setMimeType(file.getContentType());
|
||||||
orderItemRepo.save(item);
|
orderItemRepo.save(item);
|
||||||
@@ -158,9 +166,9 @@ public class OrderController {
|
|||||||
.orElseThrow(() -> new RuntimeException("Order not found"));
|
.orElseThrow(() -> new RuntimeException("Order not found"));
|
||||||
|
|
||||||
if (isConfirmation) {
|
if (isConfirmation) {
|
||||||
String relativePath = buildConfirmationPdfRelativePath(order);
|
Path relativePath = buildConfirmationPdfRelativePath(order);
|
||||||
try {
|
try {
|
||||||
byte[] existingPdf = storageService.loadAsResource(Paths.get(relativePath)).getInputStream().readAllBytes();
|
byte[] existingPdf = storageService.loadAsResource(relativePath).getInputStream().readAllBytes();
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header("Content-Disposition", "attachment; filename=\"confirmation-" + getDisplayOrderNumber(order) + ".pdf\"")
|
.header("Content-Disposition", "attachment; filename=\"confirmation-" + getDisplayOrderNumber(order) + ".pdf\"")
|
||||||
.contentType(MediaType.APPLICATION_PDF)
|
.contentType(MediaType.APPLICATION_PDF)
|
||||||
@@ -182,8 +190,13 @@ public class OrderController {
|
|||||||
.body(pdf);
|
.body(pdf);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildConfirmationPdfRelativePath(Order order) {
|
private Path buildConfirmationPdfRelativePath(Order order) {
|
||||||
return "orders/" + order.getId() + "/documents/confirmation-" + getDisplayOrderNumber(order) + ".pdf";
|
return Path.of(
|
||||||
|
"orders",
|
||||||
|
order.getId().toString(),
|
||||||
|
"documents",
|
||||||
|
"confirmation-" + getDisplayOrderNumber(order) + ".pdf"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{orderId}/twint")
|
@GetMapping("/{orderId}/twint")
|
||||||
@@ -236,13 +249,38 @@ public class OrderController {
|
|||||||
|
|
||||||
private String getExtension(String filename) {
|
private String getExtension(String filename) {
|
||||||
if (filename == null) return "stl";
|
if (filename == null) return "stl";
|
||||||
int i = filename.lastIndexOf('.');
|
String cleaned = StringUtils.cleanPath(filename);
|
||||||
if (i > 0) {
|
if (cleaned.contains("..")) {
|
||||||
return filename.substring(i + 1);
|
return "stl";
|
||||||
|
}
|
||||||
|
int i = cleaned.lastIndexOf('.');
|
||||||
|
if (i > 0 && i < cleaned.length() - 1) {
|
||||||
|
String ext = cleaned.substring(i + 1).toLowerCase(Locale.ROOT);
|
||||||
|
if (SAFE_EXTENSION_PATTERN.matcher(ext).matches()) {
|
||||||
|
return ext;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return "stl";
|
return "stl";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Path resolveOrderItemRelativePath(String storedRelativePath, UUID orderId, UUID orderItemId) {
|
||||||
|
try {
|
||||||
|
Path candidate = Path.of(storedRelativePath).normalize();
|
||||||
|
if (candidate.isAbsolute()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path expectedPrefix = Path.of("orders", orderId.toString(), "3d-files", orderItemId.toString());
|
||||||
|
if (!candidate.startsWith(expectedPrefix)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
} catch (InvalidPathException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private OrderDto convertToDto(Order order, List<OrderItem> items) {
|
private OrderDto convertToDto(Order order, List<OrderItem> items) {
|
||||||
OrderDto dto = new OrderDto();
|
OrderDto dto = new OrderDto();
|
||||||
dto.setId(order.getId());
|
dto.setId(order.getId());
|
||||||
|
|||||||
@@ -19,15 +19,19 @@ import com.printcalculator.service.SlicerService;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.InvalidPathException;
|
||||||
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.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -35,6 +39,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Locale;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.UrlResource;
|
import org.springframework.core.io.UrlResource;
|
||||||
|
|
||||||
@@ -42,6 +47,7 @@ import org.springframework.core.io.UrlResource;
|
|||||||
@RequestMapping("/api/quote-sessions")
|
@RequestMapping("/api/quote-sessions")
|
||||||
|
|
||||||
public class QuoteSessionController {
|
public class QuoteSessionController {
|
||||||
|
private static final Path QUOTE_STORAGE_ROOT = Paths.get("storage_quotes").toAbsolutePath().normalize();
|
||||||
|
|
||||||
private final QuoteSessionRepository sessionRepo;
|
private final QuoteSessionRepository sessionRepo;
|
||||||
private final QuoteLineItemRepository lineItemRepo;
|
private final QuoteLineItemRepository lineItemRepo;
|
||||||
@@ -121,19 +127,24 @@ public class QuoteSessionController {
|
|||||||
|
|
||||||
// 1. Define Persistent Storage Path
|
// 1. Define Persistent Storage Path
|
||||||
// Structure: storage_quotes/{sessionId}/{uuid}.{ext}
|
// Structure: storage_quotes/{sessionId}/{uuid}.{ext}
|
||||||
String storageDir = "storage_quotes/" + session.getId();
|
Path sessionStorageDir = QUOTE_STORAGE_ROOT.resolve(session.getId().toString()).normalize();
|
||||||
Files.createDirectories(Paths.get(storageDir));
|
if (!sessionStorageDir.startsWith(QUOTE_STORAGE_ROOT)) {
|
||||||
|
throw new IOException("Invalid quote session storage path");
|
||||||
|
}
|
||||||
|
Files.createDirectories(sessionStorageDir);
|
||||||
|
|
||||||
String originalFilename = file.getOriginalFilename();
|
String originalFilename = file.getOriginalFilename();
|
||||||
String ext = originalFilename != null && originalFilename.contains(".")
|
String ext = getSafeExtension(originalFilename, "stl");
|
||||||
? originalFilename.substring(originalFilename.lastIndexOf("."))
|
String storedFilename = UUID.randomUUID() + "." + ext;
|
||||||
: ".stl";
|
Path persistentPath = sessionStorageDir.resolve(storedFilename).normalize();
|
||||||
|
if (!persistentPath.startsWith(sessionStorageDir)) {
|
||||||
String storedFilename = UUID.randomUUID() + ext;
|
throw new IOException("Invalid quote line-item storage path");
|
||||||
Path persistentPath = Paths.get(storageDir, storedFilename);
|
}
|
||||||
|
|
||||||
// Save file
|
// Save file
|
||||||
Files.copy(file.getInputStream(), persistentPath);
|
try (InputStream inputStream = file.getInputStream()) {
|
||||||
|
Files.copy(inputStream, persistentPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Apply Basic/Advanced Logic
|
// Apply Basic/Advanced Logic
|
||||||
@@ -191,7 +202,7 @@ public class QuoteSessionController {
|
|||||||
QuoteLineItem item = new QuoteLineItem();
|
QuoteLineItem item = new QuoteLineItem();
|
||||||
item.setQuoteSession(session);
|
item.setQuoteSession(session);
|
||||||
item.setOriginalFilename(file.getOriginalFilename());
|
item.setOriginalFilename(file.getOriginalFilename());
|
||||||
item.setStoredPath(persistentPath.toString()); // SAVE PATH
|
item.setStoredPath(QUOTE_STORAGE_ROOT.relativize(persistentPath).toString()); // SAVE PATH (relative to root)
|
||||||
item.setQuantity(1);
|
item.setQuantity(1);
|
||||||
item.setColorCode(selectedVariant.getColorName());
|
item.setColorCode(selectedVariant.getColorName());
|
||||||
item.setFilamentVariant(selectedVariant);
|
item.setFilamentVariant(selectedVariant);
|
||||||
@@ -460,16 +471,54 @@ public class QuoteSessionController {
|
|||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
Path path = Paths.get(item.getStoredPath());
|
Path path = resolveStoredQuotePath(item.getStoredPath(), sessionId);
|
||||||
if (!Files.exists(path)) {
|
if (path == null || !Files.exists(path)) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
org.springframework.core.io.Resource resource = new org.springframework.core.io.UrlResource(path.toUri());
|
org.springframework.core.io.Resource resource = new UrlResource(path.toUri());
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
.header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + item.getOriginalFilename() + "\"")
|
.header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + item.getOriginalFilename() + "\"")
|
||||||
.body(resource);
|
.body(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getSafeExtension(String filename, String fallback) {
|
||||||
|
if (filename == null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
String cleaned = StringUtils.cleanPath(filename);
|
||||||
|
if (cleaned.contains("..")) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
int index = cleaned.lastIndexOf('.');
|
||||||
|
if (index <= 0 || index >= cleaned.length() - 1) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
String ext = cleaned.substring(index + 1).toLowerCase(Locale.ROOT);
|
||||||
|
return switch (ext) {
|
||||||
|
case "stl" -> "stl";
|
||||||
|
case "3mf" -> "3mf";
|
||||||
|
case "step", "stp" -> "step";
|
||||||
|
default -> fallback;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolveStoredQuotePath(String storedPath, UUID expectedSessionId) {
|
||||||
|
if (storedPath == null || storedPath.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Path raw = Path.of(storedPath).normalize();
|
||||||
|
Path resolved = raw.isAbsolute() ? raw : QUOTE_STORAGE_ROOT.resolve(raw).normalize();
|
||||||
|
Path expectedSessionRoot = QUOTE_STORAGE_ROOT.resolve(expectedSessionId.toString()).normalize();
|
||||||
|
if (!resolved.startsWith(expectedSessionRoot)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
} catch (InvalidPathException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user