# 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`](cannamanage-service/src/main/java/de/cannamanage/service/DocumentService.java:62) **Rule:** OWASP A01:2021 — Broken Access Control ```java 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: ```java 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-api/src/main/resources/application.properties:8) ```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`](cannamanage-api/src/main/java/de/cannamanage/api/controller/AuthController.java:30) 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`](cannamanage-api/src/main/java/de/cannamanage/api/security/SecurityConfig.java:131) 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`](cannamanage-service/src/main/java/de/cannamanage/service/report/AuthorityExportService.java:104) ```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