Backend: - ComplianceDashboardService: traffic-light status per ComplianceArea (KCANG/FINANCE/DSGVO/VEREIN) based on deadlines, payments, board positions - RetentionService: scheduled anonymization of expired member data (KCanG §24, 5 years), with dry-run preview and retention report endpoints - ComplianceDeadlineSeeder: seeds 5 standard recurring deadlines on club creation - ComplianceDashboardController: GET /api/v1/compliance/dashboard, GET /retention, POST /retention/preview - Repository additions: countOverdue, countActive board positions/members Frontend: - /compliance page with traffic-light status cards per area - Overdue deadlines section (highlighted red) with 'days overdue' badges - Upcoming deadlines with 'days until due' badges and 'Complete' buttons - Retention info cards (KCanG §24: 5y, AO §147: 10y, DSGVO: 2y) - Navigation: added 'Compliance-Status' to sidebar under Compliance group - compliance-dashboard.ts service with mock data for dev mode Build verified: pnpm build passes clean.
9.9 KiB
Security Scan Report: CannaManage (Sprint 7–9 Focus)
Date: 2026-06-15
Reviewer: Roo (Security Reviewer)
Scope: Full codebase with focus on Sprint 7–9 additions
Tools Used: SonarQube SAST (MCP), Snyk SCA, Manual Checklist
Verdict: ✅ PASS (with Medium advisories)
No Critical or High severity security vulnerabilities found.
2 Medium findings require attention in the next sprint.
1. Automated Scan Results
1.1 SonarQube SAST (Static Application Security Testing)
| File | Issues | Security Impact |
|---|---|---|
SecurityConfig.java |
4× string duplication (S1192), 1× package FP | None — maintainability only |
JwtAuthFilter.java |
1× package FP | None |
AuthController.java |
1× package FP | None |
DocumentService.java |
1× integer overflow in constant (S2184), 1× generic exception (S112), 2× hardcoded path delimiter (S1075) | Low — see findings |
Summary: 0 security vulnerabilities detected. All SAST findings are maintainability/reliability issues, not exploitable security weaknesses.
1.2 Snyk SCA (Software Composition Analysis)
| Component | Severity | Vulnerability | Fix Available |
|---|---|---|---|
spring-boot-autoconfigure@4.0.6 |
Medium | Insecure Temporary File (SNYK-JAVA-ORGSPRINGFRAMEWORKBOOT-17308346) | Upgrade to 4.0.7 |
openpdf@2.0.4 |
Medium | Dual License (LGPL-2.1/MPL-2.0) — license compliance risk | Accept or replace |
Frontend (npm): ✅ 0 vulnerabilities across 18 dependencies
Backend (Maven): 2 medium issues (1 vulnerability, 1 license)
Total projects tested: 7
2. Manual Security Checklist
| # | Check | Result | Evidence |
|---|---|---|---|
| 1 | No hardcoded secrets in source | ⚠️ Medium | JWT dev secret in application.properties line 8 — acceptable for dev profile, production uses ${CANNAMANAGE_SECURITY_JWT_SECRET} env var |
| 2 | JWT secret from environment variable only | ✅ | Production profile (application-production.properties:22) uses ${CANNAMANAGE_SECURITY_JWT_SECRET} |
| 3 | CSRF protection configured properly | ✅ | API (stateless JWT) correctly disables CSRF; Portal (session-based) uses CookieCsrfTokenRepository |
| 4 | CORS not overly permissive | ✅ | Restricted to localhost:3000 and frontend:3000 (Docker internal). Production should add production domain. |
| 5 | File upload size/type restrictions | ✅ | DocumentService.java:26-33 — 10MB max, allowlist: PDF/DOCX/XLSX/PNG/JPG |
| 6 | SQL injection prevention | ✅ | All queries use JPQL with named parameters (:param). No native queries with string concatenation. 12 @Query annotations reviewed — all parameterized. |
| 7 | Path traversal prevention in DocumentService | ⚠️ Medium | DocumentService.java:62 — filename from file.getOriginalFilename() is used in path construction without sanitization. UUID prefix mitigates exploitation but the original filename is concatenated directly. |
| 8 | Rate limiting on sensitive endpoints | ✅ | Authority export: 1/hour per tenant (AuthorityExportService.java:76-79). Email: rate-limited to 50/min. Login: no explicit rate limit but mitigated by BCrypt cost factor. |
| 9 | Password hashing with BCrypt | ✅ | SecurityConfig.java:124 — BCryptPasswordEncoder. Password validation with complexity regex in SetPasswordRequest.java. |
| 10 | Tenant isolation (club_id filtering) | ✅ | All service methods accept clubId from TenantContext.getCurrentTenant() (JWT claim, not user input). Portal endpoints derive memberId from authenticated userId. |
3. Detailed Findings
3.1 ⚠️ Medium: Path Traversal Risk in DocumentService
File: DocumentService.java
Rule: OWASP A01:2021 — Broken Access Control
String filename = file.getOriginalFilename() != null ? file.getOriginalFilename() : "document";
String storagePath = clubId + "/" + documentId + "_" + filename;
Path fullPath = Paths.get(UPLOAD_BASE, storagePath);
Risk: A malicious filename like ../../../etc/passwd could theoretically escape the upload directory. The UUID prefix (documentId + "_") and the fact that clubId is server-controlled reduce exploitability, but the original filename is not sanitized for path separators.
Recommendation: Add filename sanitization:
String sanitized = Paths.get(filename).getFileName().toString();
// or: filename.replaceAll("[^a-zA-Z0-9.\\-_]", "_");
Exploitability: Low (UUID prefix + server-controlled clubId make it very hard to construct a useful path traversal), but defense-in-depth principle applies.
3.2 ⚠️ Medium: JWT Dev Secret in Default Properties
File: application.properties
cannamanage.security.jwt.secret=Y2FubmFtYW5hZ2Utand0LXNlY3JldC1rZXktZm9yLWRldmVsb3BtZW50LW9ubHktMzI=
Risk: This is a base64-encoded development-only secret. Production correctly overrides via ${CANNAMANAGE_SECURITY_JWT_SECRET}. However, if the production environment variable is ever missing, Spring Boot falls back to this known value.
Recommendation:
- Add a startup check that fails if running with
productionprofile and the default secret is detected - Or remove the default and require the env var in all profiles
3.3 ⚠️ Medium: Spring Boot Insecure Temporary File (CVE)
Component: spring-boot-autoconfigure@4.0.6
Fix: Upgrade to Spring Boot 4.0.7
Impact: Temporary file creation may use insecure permissions on some OS configurations.
3.4 ℹ️ Low: No Login Rate Limiting
File: AuthController.java
The /api/v1/auth/login endpoint has no explicit rate limiting. BCrypt's computational cost provides some natural brute-force resistance (~100ms per attempt), but a dedicated rate limiter (e.g., Bucket4j or Spring Security's AuthenticationFailureHandler with exponential backoff) would strengthen defense.
Recommendation: Add rate limiting: max 5 failed attempts per IP per 15 minutes.
3.5 ℹ️ Low: CORS Missing Production Domain
File: SecurityConfig.java
CORS allowedOrigins only includes localhost:3000 and frontend:3000. The production domain (cannamanage.plate-software.de) is not listed. This is likely handled by the reverse proxy (nginx), but if the API is ever accessed directly, CORS will block legitimate requests.
Recommendation: Make CORS origins configurable via @Value from application properties.
3.6 ℹ️ Low: AuthorityExportService — JSON Injection in Audit Log
File: AuthorityExportService.java
"{\"year\":" + year + ",\"reason\":\"" + escapeJson(reason) + "\"}"
The reason field is escaped via escapeJson(), which is good. However, manual JSON construction is fragile. Consider using a proper JSON library (Jackson ObjectMapper) for audit metadata serialization.
4. Security Architecture Assessment
Strengths ✅
- Multi-layer authentication: JWT for API, session-based for portal, re-authentication for sensitive exports
- RBAC with granular permissions: 23+
StaffPermissionenum values, checked viaStaffPermissionChecker - Tenant isolation:
TenantContextfrom JWT claims, not user-controllable input - Token revocation: JTI-based blacklist checked on every request
- Append-only financial data:
LedgerEntryper §147 AO — cannot delete or modify - Audit trail: Comprehensive
AuditService.log()calls on all sensitive operations - File upload validation: Size limit + content-type allowlist + UUID-based storage paths
- Production hardening: Error details hidden (
server.error.include-message=never), Swagger disabled, minimal actuator exposure - Session security:
httpOnly=true,sameSite=strict, 30min timeout, max 1 concurrent session - Secure error messages:
GlobalExceptionHandlerreturns generic messages, no stack traces
Areas for Improvement 📋
- Add explicit login rate limiting (Bucket4j or similar)
- Sanitize original filename in
DocumentService - Upgrade Spring Boot to 4.0.7
- Make CORS origins environment-configurable
- Add Content-Security-Policy headers
- Consider adding request signing for webhook endpoints (
/api/v1/webhooks/**)
5. Compliance Notes
| Standard | Status | Notes |
|---|---|---|
| OWASP Top 10 (2021) | ✅ Good | No A01-A10 critical findings |
| DSGVO/GDPR | ✅ Good | PII minimization in authority export (anonymized member list), audit trail |
| §147 AO (Aufbewahrung) | ✅ Good | Append-only ledger, no deletion of financial records |
| KCanG (Cannabis law) | ✅ Good | Compliance deadlines, quantity tracking, authority reporting |
6. Summary Table
| Severity | Count | Action Required |
|---|---|---|
| Critical | 0 | — |
| High | 0 | — |
| Medium | 3 | Fix in next sprint (path traversal sanitization, JWT fallback guard, Spring Boot upgrade) |
| Low | 3 | Advisory — address when convenient |
Verdict
✅ PASS
No Critical or High severity findings. The application demonstrates strong security architecture with proper authentication, authorization, tenant isolation, and audit logging. The 3 Medium findings are defense-in-depth improvements, not actively exploitable vulnerabilities.
Recommended next actions (priority order):
mvn versions:set -DnewVersion=...— upgrade Spring Boot to 4.0.7- Add
Paths.get(filename).getFileName().toString()sanitization in DocumentService - Add startup validation that rejects the default JWT secret in production profile