dev #18

Merged
JoeKung merged 2 commits from dev into main 2026-03-04 15:03:12 +01:00
8 changed files with 539 additions and 30 deletions
Showing only changes of commit 6f47d02813 - Show all commits

View File

@@ -27,6 +27,7 @@ import java.util.UUID;
import java.util.Map; import java.util.Map;
import java.util.HashMap; import java.util.HashMap;
import java.util.Base64; import java.util.Base64;
import java.util.Set;
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.Locale;
@@ -36,6 +37,11 @@ import java.util.regex.Pattern;
@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 static final Pattern SAFE_EXTENSION_PATTERN = Pattern.compile("^[a-z0-9]{1,10}$");
private static final Set<String> PERSONAL_DATA_REDACTED_STATUSES = Set.of(
"IN_PRODUCTION",
"SHIPPED",
"COMPLETED"
);
private final OrderService orderService; private final OrderService orderService;
private final OrderRepository orderRepo; private final OrderRepository orderRepo;
@@ -292,10 +298,13 @@ public class OrderController {
dto.setPaymentMethod(p.getMethod()); dto.setPaymentMethod(p.getMethod());
}); });
boolean redactPersonalData = shouldRedactPersonalData(order.getStatus());
if (!redactPersonalData) {
dto.setCustomerEmail(order.getCustomerEmail()); dto.setCustomerEmail(order.getCustomerEmail());
dto.setCustomerPhone(order.getCustomerPhone()); dto.setCustomerPhone(order.getCustomerPhone());
dto.setPreferredLanguage(order.getPreferredLanguage());
dto.setBillingCustomerType(order.getBillingCustomerType()); dto.setBillingCustomerType(order.getBillingCustomerType());
}
dto.setPreferredLanguage(order.getPreferredLanguage());
dto.setCurrency(order.getCurrency()); dto.setCurrency(order.getCurrency());
dto.setSetupCostChf(order.getSetupCostChf()); dto.setSetupCostChf(order.getSetupCostChf());
dto.setShippingCostChf(order.getShippingCostChf()); dto.setShippingCostChf(order.getShippingCostChf());
@@ -310,6 +319,7 @@ public class OrderController {
dto.setCreatedAt(order.getCreatedAt()); dto.setCreatedAt(order.getCreatedAt());
dto.setShippingSameAsBilling(order.getShippingSameAsBilling()); dto.setShippingSameAsBilling(order.getShippingSameAsBilling());
if (!redactPersonalData) {
AddressDto billing = new AddressDto(); AddressDto billing = new AddressDto();
billing.setFirstName(order.getBillingFirstName()); billing.setFirstName(order.getBillingFirstName());
billing.setLastName(order.getBillingLastName()); billing.setLastName(order.getBillingLastName());
@@ -335,6 +345,7 @@ public class OrderController {
shipping.setCountryCode(order.getShippingCountryCode()); shipping.setCountryCode(order.getShippingCountryCode());
dto.setShippingAddress(shipping); dto.setShippingAddress(shipping);
} }
}
List<OrderItemDto> itemDtos = items.stream().map(i -> { List<OrderItemDto> itemDtos = items.stream().map(i -> {
OrderItemDto idto = new OrderItemDto(); OrderItemDto idto = new OrderItemDto();
@@ -354,6 +365,13 @@ public class OrderController {
return dto; return dto;
} }
private boolean shouldRedactPersonalData(String status) {
if (status == null || status.isBlank()) {
return false;
}
return PERSONAL_DATA_REDACTED_STATUSES.contains(status.trim().toUpperCase(Locale.ROOT));
}
private String getDisplayOrderNumber(Order order) { private String getDisplayOrderNumber(Order order) {
String orderNumber = order.getOrderNumber(); String orderNumber = order.getOrderNumber();
if (orderNumber != null && !orderNumber.isBlank()) { if (orderNumber != null && !orderNumber.isBlank()) {

View File

@@ -8,6 +8,7 @@ import com.printcalculator.entity.Order;
import com.printcalculator.entity.OrderItem; import com.printcalculator.entity.OrderItem;
import com.printcalculator.entity.Payment; import com.printcalculator.entity.Payment;
import com.printcalculator.entity.QuoteSession; import com.printcalculator.entity.QuoteSession;
import com.printcalculator.event.OrderShippedEvent;
import com.printcalculator.repository.OrderItemRepository; import com.printcalculator.repository.OrderItemRepository;
import com.printcalculator.repository.OrderRepository; import com.printcalculator.repository.OrderRepository;
import com.printcalculator.repository.PaymentRepository; import com.printcalculator.repository.PaymentRepository;
@@ -15,6 +16,7 @@ import com.printcalculator.service.InvoicePdfRenderingService;
import com.printcalculator.service.PaymentService; import com.printcalculator.service.PaymentService;
import com.printcalculator.service.QrBillService; import com.printcalculator.service.QrBillService;
import com.printcalculator.service.StorageService; import com.printcalculator.service.StorageService;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.ContentDisposition; import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -61,6 +63,7 @@ public class AdminOrderController {
private final StorageService storageService; private final StorageService storageService;
private final InvoicePdfRenderingService invoiceService; private final InvoicePdfRenderingService invoiceService;
private final QrBillService qrBillService; private final QrBillService qrBillService;
private final ApplicationEventPublisher eventPublisher;
public AdminOrderController( public AdminOrderController(
OrderRepository orderRepo, OrderRepository orderRepo,
@@ -69,7 +72,8 @@ public class AdminOrderController {
PaymentService paymentService, PaymentService paymentService,
StorageService storageService, StorageService storageService,
InvoicePdfRenderingService invoiceService, InvoicePdfRenderingService invoiceService,
QrBillService qrBillService QrBillService qrBillService,
ApplicationEventPublisher eventPublisher
) { ) {
this.orderRepo = orderRepo; this.orderRepo = orderRepo;
this.orderItemRepo = orderItemRepo; this.orderItemRepo = orderItemRepo;
@@ -78,6 +82,7 @@ public class AdminOrderController {
this.storageService = storageService; this.storageService = storageService;
this.invoiceService = invoiceService; this.invoiceService = invoiceService;
this.qrBillService = qrBillService; this.qrBillService = qrBillService;
this.eventPublisher = eventPublisher;
} }
@GetMapping @GetMapping
@@ -124,10 +129,16 @@ public class AdminOrderController {
"Invalid order status. Allowed values: " + String.join(", ", ALLOWED_ORDER_STATUSES) "Invalid order status. Allowed values: " + String.join(", ", ALLOWED_ORDER_STATUSES)
); );
} }
String previousStatus = order.getStatus();
order.setStatus(normalizedStatus); order.setStatus(normalizedStatus);
orderRepo.save(order); Order savedOrder = orderRepo.save(order);
return ResponseEntity.ok(toOrderDto(order)); // Notify customer only on transition to SHIPPED.
if (!"SHIPPED".equals(previousStatus) && "SHIPPED".equals(normalizedStatus)) {
eventPublisher.publishEvent(new OrderShippedEvent(this, savedOrder));
}
return ResponseEntity.ok(toOrderDto(savedOrder));
} }
@GetMapping("/{orderId}/items/{orderItemId}/file") @GetMapping("/{orderId}/items/{orderItemId}/file")

View File

@@ -0,0 +1,16 @@
package com.printcalculator.event;
import com.printcalculator.entity.Order;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
@Getter
public class OrderShippedEvent extends ApplicationEvent {
private final Order order;
public OrderShippedEvent(Object source, Order order) {
super(source);
this.order = order;
}
}

View File

@@ -4,6 +4,7 @@ import com.printcalculator.entity.Order;
import com.printcalculator.entity.OrderItem; import com.printcalculator.entity.OrderItem;
import com.printcalculator.entity.Payment; import com.printcalculator.entity.Payment;
import com.printcalculator.event.OrderCreatedEvent; import com.printcalculator.event.OrderCreatedEvent;
import com.printcalculator.event.OrderShippedEvent;
import com.printcalculator.event.PaymentConfirmedEvent; import com.printcalculator.event.PaymentConfirmedEvent;
import com.printcalculator.event.PaymentReportedEvent; import com.printcalculator.event.PaymentReportedEvent;
import com.printcalculator.repository.OrderItemRepository; import com.printcalculator.repository.OrderItemRepository;
@@ -95,6 +96,19 @@ public class OrderEmailListener {
} }
} }
@Async
@EventListener
public void handleOrderShippedEvent(OrderShippedEvent event) {
Order order = event.getOrder();
log.info("Processing OrderShippedEvent for order id: {}", order.getId());
try {
sendOrderShippedEmail(order);
} catch (Exception e) {
log.error("Failed to send order shipped email for order id: {}", order.getId(), e);
}
}
private void sendCustomerConfirmationEmail(Order order) { private void sendCustomerConfirmationEmail(Order order) {
String language = resolveLanguage(order.getPreferredLanguage()); String language = resolveLanguage(order.getPreferredLanguage());
String orderNumber = getDisplayOrderNumber(order); String orderNumber = getDisplayOrderNumber(order);
@@ -153,6 +167,21 @@ public class OrderEmailListener {
); );
} }
private void sendOrderShippedEmail(Order order) {
String language = resolveLanguage(order.getPreferredLanguage());
String orderNumber = getDisplayOrderNumber(order);
Map<String, Object> templateData = buildBaseTemplateData(order, language);
String subject = applyOrderShippedTexts(templateData, language, orderNumber);
emailNotificationService.sendEmail(
order.getCustomer().getEmail(),
subject,
"order-shipped",
templateData
);
}
private void sendAdminNotificationEmail(Order order) { private void sendAdminNotificationEmail(Order order) {
String orderNumber = getDisplayOrderNumber(order); String orderNumber = getDisplayOrderNumber(order);
Map<String, Object> templateData = buildBaseTemplateData(order, DEFAULT_LANGUAGE); Map<String, Object> templateData = buildBaseTemplateData(order, DEFAULT_LANGUAGE);
@@ -381,6 +410,63 @@ public class OrderEmailListener {
}; };
} }
private String applyOrderShippedTexts(Map<String, Object> templateData, String language, String orderNumber) {
return switch (language) {
case "en" -> {
templateData.put("emailTitle", "Order Shipped");
templateData.put("headlineText", "Your order #" + orderNumber + " has been shipped");
templateData.put("greetingText", "Hi " + templateData.get("customerName") + ",");
templateData.put("introText", "Good news: your package has left our workshop and is on its way.");
templateData.put("statusText", "Current status: Shipped.");
templateData.put("orderDetailsCtaText", "View order status");
templateData.put("supportText", "If you need assistance, reply to this email.");
templateData.put("footerText", "Automated message from 3D-Fab.");
templateData.put("labelOrderNumber", "Order number");
templateData.put("labelTotal", "Total");
yield "Your order has been shipped (Order #" + orderNumber + ") - 3D-Fab";
}
case "de" -> {
templateData.put("emailTitle", "Bestellung versandt");
templateData.put("headlineText", "Ihre Bestellung #" + orderNumber + " wurde versandt");
templateData.put("greetingText", "Hallo " + templateData.get("customerName") + ",");
templateData.put("introText", "Gute Nachricht: Ihr Paket hat unsere Werkstatt verlassen und ist unterwegs.");
templateData.put("statusText", "Aktueller Status: Versandt.");
templateData.put("orderDetailsCtaText", "Bestellstatus ansehen");
templateData.put("supportText", "Wenn Sie Hilfe benoetigen, antworten Sie auf diese E-Mail.");
templateData.put("footerText", "Automatische Nachricht von 3D-Fab.");
templateData.put("labelOrderNumber", "Bestellnummer");
templateData.put("labelTotal", "Gesamtbetrag");
yield "Ihre Bestellung wurde versandt (Bestellung #" + orderNumber + ") - 3D-Fab";
}
case "fr" -> {
templateData.put("emailTitle", "Commande expediee");
templateData.put("headlineText", "Votre commande #" + orderNumber + " a ete expediee");
templateData.put("greetingText", "Bonjour " + templateData.get("customerName") + ",");
templateData.put("introText", "Bonne nouvelle: votre colis a quitte notre atelier et est en route.");
templateData.put("statusText", "Statut actuel: Expediee.");
templateData.put("orderDetailsCtaText", "Voir le statut de la commande");
templateData.put("supportText", "Si vous avez besoin d'aide, repondez a cet email.");
templateData.put("footerText", "Message automatique de 3D-Fab.");
templateData.put("labelOrderNumber", "Numero de commande");
templateData.put("labelTotal", "Total");
yield "Votre commande a ete expediee (Commande #" + orderNumber + ") - 3D-Fab";
}
default -> {
templateData.put("emailTitle", "Ordine spedito");
templateData.put("headlineText", "Il tuo ordine #" + orderNumber + " e' stato spedito");
templateData.put("greetingText", "Ciao " + templateData.get("customerName") + ",");
templateData.put("introText", "Buone notizie: il tuo pacco e' partito dal nostro laboratorio ed e' in viaggio.");
templateData.put("statusText", "Stato attuale: spedito.");
templateData.put("orderDetailsCtaText", "Visualizza stato ordine");
templateData.put("supportText", "Se hai bisogno di assistenza, rispondi a questa email.");
templateData.put("footerText", "Messaggio automatico di 3D-Fab.");
templateData.put("labelOrderNumber", "Numero ordine");
templateData.put("labelTotal", "Totale");
yield "Il tuo ordine e' stato spedito (Ordine #" + orderNumber + ") - 3D-Fab";
}
};
}
private String getDisplayOrderNumber(Order order) { private String getDisplayOrderNumber(Order order) {
String orderNumber = order.getOrderNumber(); String orderNumber = order.getOrderNumber();
if (orderNumber != null && !orderNumber.isBlank()) { if (orderNumber != null && !orderNumber.isBlank()) {

View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="${emailTitle}">Order Shipped</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 20px auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
border-bottom: 1px solid #eeeeee;
padding-bottom: 20px;
margin-bottom: 20px;
}
.header h1 {
color: #333333;
}
.content {
color: #555555;
line-height: 1.6;
}
.status-box {
background-color: #e9f3ff;
border: 1px solid #a9c8ef;
border-radius: 5px;
padding: 12px;
margin-top: 18px;
}
.order-box {
background-color: #f9f9f9;
border-radius: 5px;
padding: 12px;
margin-top: 18px;
}
.order-box th {
text-align: left;
padding-right: 18px;
vertical-align: top;
}
.footer {
text-align: center;
font-size: 0.9em;
color: #999999;
margin-top: 30px;
border-top: 1px solid #eeeeee;
padding-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1 th:text="${headlineText}">Your order #00000000 has been shipped</h1>
</div>
<div class="content">
<p th:text="${greetingText}">Hi Customer,</p>
<p th:text="${introText}">Good news: your package is on its way.</p>
<div class="status-box">
<strong th:text="${statusText}">Current status: Shipped.</strong>
</div>
<div class="order-box">
<table>
<tr>
<th th:text="${labelOrderNumber}">Order number</th>
<td th:text="${orderNumber}">00000000</td>
</tr>
<tr>
<th th:text="${labelTotal}">Total</th>
<td th:text="${totalCost}">CHF 0.00</td>
</tr>
</table>
</div>
<p>
<span th:text="${orderDetailsCtaText}">View order status</span>:
<a th:href="${orderDetailsUrl}" th:text="${orderDetailsUrl}">https://example.com/en/co/00000000-0000-0000-0000-000000000000</a>
</p>
<p th:text="${supportText}">If you need assistance, reply to this email.</p>
</div>
<div class="footer">
<p>&copy; <span th:text="${currentYear}">2026</span> 3D-Fab</p>
<p th:text="${footerText}">Automated message.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,140 @@
package com.printcalculator.controller;
import com.printcalculator.dto.OrderDto;
import com.printcalculator.entity.Order;
import com.printcalculator.repository.CustomerRepository;
import com.printcalculator.repository.OrderItemRepository;
import com.printcalculator.repository.OrderRepository;
import com.printcalculator.repository.PaymentRepository;
import com.printcalculator.repository.QuoteLineItemRepository;
import com.printcalculator.repository.QuoteSessionRepository;
import com.printcalculator.service.InvoicePdfRenderingService;
import com.printcalculator.service.OrderService;
import com.printcalculator.service.PaymentService;
import com.printcalculator.service.QrBillService;
import com.printcalculator.service.StorageService;
import com.printcalculator.service.TwintPaymentService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class OrderControllerPrivacyTest {
@Mock
private OrderService orderService;
@Mock
private OrderRepository orderRepo;
@Mock
private OrderItemRepository orderItemRepo;
@Mock
private QuoteSessionRepository quoteSessionRepo;
@Mock
private QuoteLineItemRepository quoteLineItemRepo;
@Mock
private CustomerRepository customerRepo;
@Mock
private StorageService storageService;
@Mock
private InvoicePdfRenderingService invoiceService;
@Mock
private QrBillService qrBillService;
@Mock
private TwintPaymentService twintPaymentService;
@Mock
private PaymentService paymentService;
@Mock
private PaymentRepository paymentRepo;
private OrderController controller;
@BeforeEach
void setUp() {
controller = new OrderController(
orderService,
orderRepo,
orderItemRepo,
quoteSessionRepo,
quoteLineItemRepo,
customerRepo,
storageService,
invoiceService,
qrBillService,
twintPaymentService,
paymentService,
paymentRepo
);
}
@Test
void getOrder_pendingPayment_keepsPersonalData() {
UUID orderId = UUID.randomUUID();
Order order = buildOrder(orderId, "PENDING_PAYMENT");
when(orderRepo.findById(orderId)).thenReturn(Optional.of(order));
when(orderItemRepo.findByOrder_Id(orderId)).thenReturn(List.of());
when(paymentRepo.findByOrder_Id(orderId)).thenReturn(Optional.empty());
ResponseEntity<OrderDto> response = controller.getOrder(orderId);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertEquals("customer@example.com", response.getBody().getCustomerEmail());
assertEquals("+41790000000", response.getBody().getCustomerPhone());
assertNotNull(response.getBody().getBillingAddress());
}
@Test
void getOrder_advancedStatuses_redactsPersonalData() {
List<String> statuses = List.of("IN_PRODUCTION", "SHIPPED", "COMPLETED");
for (String status : statuses) {
UUID orderId = UUID.randomUUID();
Order order = buildOrder(orderId, status);
when(orderRepo.findById(orderId)).thenReturn(Optional.of(order));
when(orderItemRepo.findByOrder_Id(orderId)).thenReturn(List.of());
when(paymentRepo.findByOrder_Id(orderId)).thenReturn(Optional.empty());
ResponseEntity<OrderDto> response = controller.getOrder(orderId);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertNull(response.getBody().getCustomerEmail());
assertNull(response.getBody().getCustomerPhone());
assertNull(response.getBody().getBillingCustomerType());
assertNull(response.getBody().getBillingAddress());
assertNull(response.getBody().getShippingAddress());
}
}
private Order buildOrder(UUID orderId, String status) {
Order order = new Order();
order.setId(orderId);
order.setStatus(status);
order.setCustomerEmail("customer@example.com");
order.setCustomerPhone("+41790000000");
order.setBillingCustomerType("PRIVATE");
order.setBillingFirstName("Joe");
order.setBillingLastName("Kung");
order.setBillingAddressLine1("Via G. Pioda 1");
order.setBillingZip("6900");
order.setBillingCity("Lugano");
order.setBillingCountryCode("CH");
order.setShippingSameAsBilling(true);
return order;
}
}

View File

@@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
@@ -47,6 +48,8 @@ class AdminOrderControllerStatusValidationTest {
private InvoicePdfRenderingService invoicePdfRenderingService; private InvoicePdfRenderingService invoicePdfRenderingService;
@Mock @Mock
private QrBillService qrBillService; private QrBillService qrBillService;
@Mock
private ApplicationEventPublisher eventPublisher;
private AdminOrderController controller; private AdminOrderController controller;
@@ -59,7 +62,8 @@ class AdminOrderControllerStatusValidationTest {
paymentService, paymentService,
storageService, storageService,
invoicePdfRenderingService, invoicePdfRenderingService,
qrBillService qrBillService,
eventPublisher
); );
} }
@@ -92,6 +96,7 @@ class AdminOrderControllerStatusValidationTest {
order.setStatus("PENDING_PAYMENT"); order.setStatus("PENDING_PAYMENT");
when(orderRepository.findById(orderId)).thenReturn(Optional.of(order)); when(orderRepository.findById(orderId)).thenReturn(Optional.of(order));
when(orderRepository.save(any(Order.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(orderItemRepository.findByOrder_Id(orderId)).thenReturn(List.of()); when(orderItemRepository.findByOrder_Id(orderId)).thenReturn(List.of());
when(paymentRepository.findByOrder_Id(orderId)).thenReturn(Optional.empty()); when(paymentRepository.findByOrder_Id(orderId)).thenReturn(Optional.empty());

View File

@@ -0,0 +1,123 @@
# Security Best Practices Report
## Executive summary
Revisione sicurezza del progetto `print-calculator` (backend Spring Boot Java + frontend Angular/TypeScript) con focus su autenticazione/autorizzazione, esposizione dati, upload file, hardening e resilienza.
Risultato: **6 finding** totali.
- **Critical**: 1
- **High**: 3
- **Medium**: 2
Rischio principale: API pubbliche basate su UUID senza controllo di ownership/token, che consentono lettura PII e azioni di business su ordini.
## Scope e metodo
- Codice analizzato: backend (`backend/src/main/java`, `backend/src/main/resources`), frontend (`frontend/src/app`), config deploy (`deploy/`, `docker-compose*.yml`).
- Riferimenti skill usati: `javascript-general-web-frontend-security.md`.
- Nota: non è presente un riferimento specifico Java/Spring nel set dello skill; per il backend sono state applicate best practice consolidate Spring/security engineering.
## Critical findings
### SBP-001 - Broken access control su API ordine pubbliche (lettura PII + azioni stato)
- **Severity**: Critical
- **Impatto**: Chiunque ottenga un `orderId` può leggere dati personali ordine e invocare operazioni di business senza autenticazione.
- **Evidenze**:
- `backend/src/main/java/com/printcalculator/config/SecurityConfig.java:36` (`.anyRequest().permitAll()`).
- `backend/src/main/java/com/printcalculator/controller/OrderController.java:131` (`GET /api/orders/{orderId}`).
- `backend/src/main/java/com/printcalculator/controller/OrderController.java:141` (`POST /api/orders/{orderId}/payments/report`).
- `backend/src/main/java/com/printcalculator/controller/OrderController.java:93` (`POST /api/orders/{orderId}/items/{orderItemId}/file`).
- `backend/src/main/java/com/printcalculator/controller/OrderController.java:295`-`337` (PII completa nel DTO: email, telefono, indirizzi billing/shipping).
- `backend/src/main/java/com/printcalculator/service/PaymentService.java:53`-`75` (cambio stato pagamento a `REPORTED`).
- **Rischio tecnico**:
- Assenza di autenticazione/authorization applicativa su endpoint ordine.
- Modello “capability by UUID” senza token secondario, expiry o binding utente.
- **Fix raccomandato**:
- Introdurre un `order_access_token` random ad alta entropia (>=128 bit), memorizzato hashato e richiesto sugli endpoint pubblici ordine.
- Separare endpoint pubblici (minimo set dati) da endpoint interni/admin.
- Rimuovere `orderItemId` e dettagli sensibili dal DTO pubblico, o usare URL firmate a scadenza per upload/download.
- Valutare auth customer leggera (magic link OTP) per consultazione/modifica ordine.
## High findings
### SBP-002 - Esposizione PII su endpoint pubblico custom quote request
- **Severity**: High
- **Evidenze**:
- `backend/src/main/java/com/printcalculator/controller/CustomQuoteRequestController.java:188`-`193` (`GET /api/custom-quote-requests/{id}` senza auth).
- `backend/src/main/java/com/printcalculator/entity/CustomQuoteRequest.java:24`-`40` (campi PII e messaggio cliente).
- `backend/src/main/java/com/printcalculator/config/SecurityConfig.java:36` (`.anyRequest().permitAll()`).
- **Rischio tecnico**:
- Endpoint “lookup by UUID” ritorna oggetto completo con dati personali.
- **Fix raccomandato**:
- Proteggere endpoint con token di accesso separato per richiesta (non solo UUID).
- Restituire una vista redatta/minimale per endpoint pubblici.
- Se endpoint non usato dal frontend, rimuoverlo.
### SBP-003 - Antivirus in fail-open + default scanner disattivato
- **Severity**: High
- **Evidenze**:
- `backend/src/main/resources/application.properties:27` (`clamav.enabled=${CLAMAV_ENABLED:false}`).
- `backend/src/main/java/com/printcalculator/service/ClamAVService.java:42`-`43` (scanner disabilitato => ritorna `true`).
- `backend/src/main/java/com/printcalculator/service/ClamAVService.java:54`-`61` (errori scanner => `FAIL-OPEN`).
- `backend/src/main/java/com/printcalculator/service/FileSystemStorageService.java:59`-`60` (eccezioni scanner ignorate, file mantenuto).
- **Rischio tecnico**:
- File malevoli possono essere accettati quando scanner è down/non configurato.
- **Fix raccomandato**:
- Policy fail-closed in ambienti non-dev (`reject on scan error`).
- Rendere `CLAMAV_ENABLED=true` default in deploy runtime e bloccare startup se scanner richiesto ma non raggiungibile.
- Telemetria/alerting su scan bypass e failure rate.
### SBP-004 - Endpoint costosi esposti senza throttling/rate limit (DoS applicativo)
- **Severity**: High
- **Evidenze**:
- `backend/src/main/java/com/printcalculator/config/SecurityConfig.java:36` (endpoint pubblici permessi globalmente).
- `backend/src/main/java/com/printcalculator/controller/QuoteController.java:38`-`39` (`POST /api/quote` pubblico).
- `backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java:114`-`120` (`POST /api/quote-sessions/{id}/line-items` pubblico).
- `backend/src/main/java/com/printcalculator/controller/QuoteSessionController.java:228`-`235` (invocazione slicing).
- `backend/src/main/java/com/printcalculator/service/SlicerService.java:156`-`163` (job fino a 5 minuti).
- **Rischio tecnico**:
- Upload/slicing massivo può saturare CPU, I/O e worker thread.
- **Fix raccomandato**:
- Rate limiting per IP/fingerprint/session (anche lato reverse proxy).
- Coda asincrona con limiti di concorrenza e timeout più stretti.
- Quote per utente/sessione e limite richieste per finestra temporale.
- CAPTCHA o proof-of-work per endpoint anonimi ad alto costo.
## Medium findings
### SBP-005 - Secret/default credenziali deboli nel codice di configurazione
- **Severity**: Medium
- **Evidenze**:
- `backend/src/main/resources/application.properties:7` (`DB_PASSWORD` fallback `printcalc_secret`).
- `backend/src/main/resources/application-local.properties:7`-`8` (admin password/secret hardcoded per profilo local).
- **Rischio tecnico**:
- In caso di misconfigurazione ambientale o uso improprio profilo, vengono usati valori prevedibili.
- **Fix raccomandato**:
- Rimuovere fallback sensibili e rendere obbligatori i secret a startup.
- Spostare credenziali locali in file non versionato (`.env.local`, `.gitignore`) con template placeholder.
- Policy di secret rotation periodica.
### SBP-006 - CSRF disabilitato globalmente con autenticazione admin basata su cookie
- **Severity**: Medium
- **Evidenze**:
- `backend/src/main/java/com/printcalculator/config/SecurityConfig.java:24` (CSRF disabilitato globalmente).
- `backend/src/main/java/com/printcalculator/security/AdminSessionService.java:129`-`136` (cookie sessione admin).
- `backend/src/main/java/com/printcalculator/security/AdminSessionService.java:133`-`134` (`Secure` + `SameSite=Strict` presenti, mitigazione parziale).
- **Rischio tecnico**:
- Con auth cookie-based, la protezione CSRF andrebbe mantenuta sugli endpoint state-changing admin; `SameSite=Strict` riduce ma non elimina tutti i vettori.
- **Fix raccomandato**:
- Riabilitare CSRF almeno su `/api/admin/**` e usare token CSRF (double-submit o synchronizer token).
- Mantenere `SameSite=Strict` come difesa aggiuntiva.
## Note e assunzioni
- Alcuni endpoint pubblici sembrano progettati come flusso anonimo customer; il finding resta valido perché manca una seconda prova di possesso oltre allUUID.
- Non è stata eseguita una DAST esterna o pentest black-box; analisi effettuata su codice statico e configurazioni nel repository.