Files
cannamanage/docs/sprint-9/security-scan-results.md
T
Patrick Plate 57f418f7c9 feat(sprint9): Phase 6 — Compliance dashboard, RetentionService, testing
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.
2026-06-15 14:12:01 +02:00

9.9 KiB
Raw Blame History

Security Scan Report: CannaManage (Sprint 79 Focus)

Date: 2026-06-15
Reviewer: Roo (Security Reviewer)
Scope: Full codebase with focus on Sprint 79 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:124BCryptPasswordEncoder. 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 production profile 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

  1. Multi-layer authentication: JWT for API, session-based for portal, re-authentication for sensitive exports
  2. RBAC with granular permissions: 23+ StaffPermission enum values, checked via StaffPermissionChecker
  3. Tenant isolation: TenantContext from JWT claims, not user-controllable input
  4. Token revocation: JTI-based blacklist checked on every request
  5. Append-only financial data: LedgerEntry per §147 AO — cannot delete or modify
  6. Audit trail: Comprehensive AuditService.log() calls on all sensitive operations
  7. File upload validation: Size limit + content-type allowlist + UUID-based storage paths
  8. Production hardening: Error details hidden (server.error.include-message=never), Swagger disabled, minimal actuator exposure
  9. Session security: httpOnly=true, sameSite=strict, 30min timeout, max 1 concurrent session
  10. Secure error messages: GlobalExceptionHandler returns generic messages, no stack traces

Areas for Improvement 📋

  1. Add explicit login rate limiting (Bucket4j or similar)
  2. Sanitize original filename in DocumentService
  3. Upgrade Spring Boot to 4.0.7
  4. Make CORS origins environment-configurable
  5. Add Content-Security-Policy headers
  6. 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):

  1. mvn versions:set -DnewVersion=... — upgrade Spring Boot to 4.0.7
  2. Add Paths.get(filename).getFileName().toString() sanitization in DocumentService
  3. Add startup validation that rejects the default JWT secret in production profile