fix(security): hardening — rate limiting, CORS config, audit safety, CSP headers, validation
- Fix 1: Login rate limiting (5 attempts/min/IP) on POST /api/v1/auth/login - New LoginRateLimiter (ConcurrentHashMap + @Scheduled reset every 60s) - HTTP 429 with German message on exceed - Client IP via X-Forwarded-For with proxy fallback - @EnableScheduling on CannaManageApplication - Fix 2: CORS origins configurable via cannamanage.cors.allowed-origins env var - Defaults to localhost + docker frontend for dev - SecurityConfig reads with @Value, splits comma-separated list - Fix 3: Audit JSON safety — replaced manual string concat with Jackson ObjectMapper - New AuditService.toMetadataJson(Map) helper - RetentionService and AuthorityExportService refactored - Fix 4: Tomcat max-http-form-post-size=2MB prevents DoS via oversized payloads - Fix 5: @Valid added to @RequestBody on 17+ endpoints across ComplianceRecordsController, FinanceController, ConsentController, StaffController, ComplianceDeadlineController, SubscriptionController, ForumController (admin + portal) - Fix 6: Content-Security-Policy 'default-src \'self\'; frame-ancestors \'none\'' + frameOptions(deny) on both API + portal filter chains
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package de.cannamanage.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.cannamanage.domain.entity.AuditEvent;
|
||||
import de.cannamanage.domain.entity.TenantContext;
|
||||
import de.cannamanage.domain.enums.AuditEventType;
|
||||
@@ -13,6 +15,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -25,6 +28,9 @@ public class AuditService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AuditService.class);
|
||||
|
||||
/** Shared ObjectMapper for safe JSON serialization of audit metadata. */
|
||||
private static final ObjectMapper METADATA_MAPPER = new ObjectMapper();
|
||||
|
||||
private final AuditEventRepository auditEventRepository;
|
||||
private final PdfReportGenerator pdfReportGenerator;
|
||||
|
||||
@@ -33,6 +39,23 @@ public class AuditService {
|
||||
this.pdfReportGenerator = pdfReportGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the given metadata map to JSON safely (proper escaping of quotes,
|
||||
* newlines, unicode). Returns {@code null} for a null/empty input and falls
|
||||
* back to {@code "{}"} if serialization fails (never throws).
|
||||
*/
|
||||
public static String toMetadataJson(Map<String, ?> metadata) {
|
||||
if (metadata == null || metadata.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return METADATA_MAPPER.writeValueAsString(metadata);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("Failed to serialize audit metadata; storing empty object instead: {}", e.getMessage());
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an immutable audit event. Once persisted, it cannot be modified or deleted.
|
||||
*/
|
||||
|
||||
@@ -170,7 +170,7 @@ public class RetentionService {
|
||||
"SYSTEM",
|
||||
"RETENTION_SERVICE",
|
||||
"KCanG §24: Member data anonymized after 5-year retention period",
|
||||
"{\"membershipNumber\":\"" + member.getMembershipNumber() + "\"}",
|
||||
AuditService.toMetadataJson(Map.of("membershipNumber", member.getMembershipNumber())),
|
||||
null
|
||||
);
|
||||
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ import de.cannamanage.domain.enums.AuditEventType;
|
||||
import de.cannamanage.domain.enums.ExportFormat;
|
||||
import de.cannamanage.domain.enums.ReportType;
|
||||
import de.cannamanage.service.AuditService;
|
||||
import java.util.Map;
|
||||
import de.cannamanage.service.repository.ClubRepository;
|
||||
import de.cannamanage.service.repository.MemberRepository;
|
||||
import org.slf4j.Logger;
|
||||
@@ -101,7 +102,7 @@ public class AuthorityExportService {
|
||||
"Club", clubId, userId,
|
||||
"System", "ADMIN",
|
||||
"Behörden-Export generiert. Grund: " + reason + ". Jahr: " + year,
|
||||
"{\"year\":" + year + ",\"reason\":\"" + escapeJson(reason) + "\"}",
|
||||
AuditService.toMetadataJson(Map.of("year", year, "reason", reason)),
|
||||
null
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user