57f418f7c9
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.
199 lines
9.9 KiB
Markdown
199 lines
9.9 KiB
Markdown
# 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
|