|
| 1 | +# TestingPlatform Enhancement Analysis |
| 2 | + |
| 3 | +Comprehensive audit covering security, feature accuracy, result consistency, and architecture. |
| 4 | +Findings are deduplicated and organized by priority. |
| 5 | + |
| 6 | +## Progress Tracker |
| 7 | + |
| 8 | +**FIXED** (16 commits on `dev`): |
| 9 | +- [x] #1 — Committed credentials removed, .gitignore updated |
| 10 | +- [x] #2 — Auth added to all InfraTesting API endpoints |
| 11 | +- [x] #3 + #10 — File size validation fixed (len()), validators use ValidationError |
| 12 | +- [x] #4 + #6 — CSRF restored on form endpoint, SameSite=Lax, CORS middleware repositioned |
| 13 | +- [x] #5 — SECRET_KEY auto-generated for dev, DEBUG defaults False, ZAP key emptied |
| 14 | +- [x] #7 — API views use serializer.validated_data consistently |
| 15 | +- [x] #8 — ScopedRateThrottle with per-endpoint scopes (auth/infra/expensive/admin) |
| 16 | +- [x] #13 — Email check reports errors instead of silent {} fallback, 60s timeout added |
| 17 | +- [x] #17 — IPv6 reachability uses OR (matching IPv4 logic) |
| 18 | +- [x] #20 — CSP report cache import added, rate limiter init fixed, error messages sanitized |
| 19 | +- [x] #21 — HSTS/SSL/X-Frame-Options/nosniff security headers (production-gated) |
| 20 | +- [x] #22 — UniqueConstraint on (user, domain) for UserDomain and MailDomain |
| 21 | +- [x] #26 — HSTS max-age parsing handles malformed values |
| 22 | +- [x] #27 — CorsMiddleware moved to top of middleware stack |
| 23 | +- [x] #28 — Admin URL configurable via ADMIN_URL env var |
| 24 | +- [x] #35 — CORS_ALLOWED_ORIGINS configurable via env var |
| 25 | +- [x] Bonus: Logout view broken exception handling cleaned up |
| 26 | + |
| 27 | +**REMAINING**: |
| 28 | +- [ ] #9 — Nmap execution timeout |
| 29 | +- [ ] #11 — Pandora polling (busy-wait, seed_expire=0) |
| 30 | +- [ ] #12 — DKIM inconsistent return types |
| 31 | +- [ ] #14 — DMARC upload API key in URL |
| 32 | +- [ ] #15 — SQLite for production |
| 33 | +- [ ] #16 — API always returns HTTP 200 |
| 34 | +- [ ] #18 — DNS queries without timeout |
| 35 | +- [ ] #19 — TLS version check fragile nmap parsing |
| 36 | +- [ ] #23 — Cascade delete wipes history |
| 37 | +- [ ] #24 — PDF report crashes on missing keys |
| 38 | +- [ ] #25 — Vulnerability lookup without caching |
| 39 | +- [ ] #29-#34 — Architecture/tech debt (lower priority) |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +## CRITICAL — Fix Before Any Production Use |
| 44 | + |
| 45 | +### 1. Committed Credentials in Git History |
| 46 | +- **File**: `compose/staging/.env` — Contains plaintext ONEKEY API email and password |
| 47 | +- **Impact**: Anyone with repo access has these credentials; they persist in git history even if deleted |
| 48 | +- **Fix**: Remove file from tracking, rotate credentials, add to `.gitignore` |
| 49 | + |
| 50 | +### 2. Unauthenticated Infrastructure Testing API Endpoints |
| 51 | +- **File**: `api/views.py:270-380` — All `InfraTestingXxxApiView` classes |
| 52 | +- **Impact**: No `authentication_classes` or `permission_classes`. Any anonymous user can trigger nmap scans, Pandora file analysis, DNS queries, TLS checks. Enables reconnaissance-as-a-service and resource exhaustion. |
| 53 | +- **Fix**: Add `permission_classes = [IsAuthenticated]` at minimum; add rate limiting |
| 54 | + |
| 55 | +### 3. File Size Validation Uses Wrong Function |
| 56 | +- **File**: `testing/validators.py:12` — `sys.getsizeof(file)` returns Python object overhead (~50-100 bytes), NOT actual file content size |
| 57 | +- **Impact**: File size limit (5MB) is never enforced. Arbitrarily large files can be uploaded. |
| 58 | +- **Fix**: Use `file.getbuffer().nbytes` or `len(file.getvalue())` for BytesIO objects |
| 59 | + |
| 60 | +### 4. CSRF Protection Disabled on Form Endpoints |
| 61 | +- **File**: `testing/views.py:146` (`check_website_security`), `:733` (`dmarc_upload`), `:867` (`receive_csp_report`) |
| 62 | +- **Impact**: `@csrf_exempt` on POST endpoints accepting form data. Combined with `SameSite=None` cookies (settings.py:135-136), this enables cross-site request forgery attacks. |
| 63 | +- **Fix**: Remove `@csrf_exempt` from form endpoints; use CSRF tokens. For webhooks (CSP reports, DMARC), use token-based auth instead. |
| 64 | + |
| 65 | +### 5. Insecure Default Configuration |
| 66 | +- **File**: `testing_platform/settings.py:25,28` |
| 67 | +- **Impact**: `SECRET_KEY` defaults to literal `"secret"`, `DEBUG` defaults to `True`. While there's a startup check for production, `ZAP_API_KEY` also defaults to `"123456"` (line 271). |
| 68 | +- **Fix**: Remove insecure defaults; require explicit env vars; fail fast on missing critical config |
| 69 | + |
| 70 | +### 6. SameSite=None on All Cookies |
| 71 | +- **File**: `testing_platform/settings.py:135-139` |
| 72 | +- **Impact**: `CSRF_COOKIE_SAMESITE = "None"` + `SESSION_COOKIE_SAMESITE = "None"` + `CORS_ALLOW_CREDENTIALS = True` effectively disables browser-side CSRF protections. Combined with #4, this is a direct attack vector. |
| 73 | +- **Fix**: Change to `"Lax"` (minimum) or `"Strict"`. Only use `"None"` if there's a documented cross-origin requirement. |
| 74 | + |
| 75 | +--- |
| 76 | + |
| 77 | +## HIGH — Significant Bugs and Gaps |
| 78 | + |
| 79 | +### 7. API Input Validation Bypassed |
| 80 | +- **File**: `api/views.py:280` (`InfraTestingEmailApiView`) — Reads `domain_name` directly from `request.data` without using serializer validation |
| 81 | +- **File**: `api/views.py:316` (`InfraTestingIPv6ApiView`) — Validates serializer but then reads from `request.data` anyway (ignoring validated data) |
| 82 | +- **File**: `api/views.py:332` (`InfraTestingSOAApiView`) — Creates a *new* serializer instance with raw domain string instead of using validated data |
| 83 | +- **Impact**: Unvalidated user input passed directly to DNS queries, nmap, and network operations |
| 84 | +- **Fix**: Use `serializer.validated_data['domain_name']` consistently after `is_valid()` |
| 85 | + |
| 86 | +### 8. No Rate Limiting Anywhere |
| 87 | +- **File**: `testing_platform/settings.py` — No `DEFAULT_THROTTLE_CLASSES` in `REST_FRAMEWORK` config |
| 88 | +- **Impact**: Expensive operations (nmap scans, Pandora submissions, DNS enumeration) can be triggered at unlimited rate. Enables DoS against both the platform and scanned targets. |
| 89 | +- **Fix**: Add DRF throttle classes; consider per-endpoint limits for expensive operations |
| 90 | + |
| 91 | +### 9. Nmap Execution Without Timeout |
| 92 | +- **File**: `testing/helpers.py:608-664` — `nmap.nmap_version_detection()` with `--script vulners` has no timeout |
| 93 | +- **Impact**: Scans can run indefinitely, exhausting worker threads. With no auth (#2) and no rate limits (#8), this is a DoS vector. |
| 94 | +- **Fix**: Add explicit timeouts; run in async task queue; implement per-user concurrency limits |
| 95 | + |
| 96 | +### 10. Validators Raise `Exception` Instead of `ValidationError` |
| 97 | +- **File**: `testing/validators.py:13,33,36,51` — All validation failures raise bare `Exception` |
| 98 | +- **Impact**: DRF cannot format these as 400 responses; they bubble up as 500 Internal Server Errors. Users see generic error pages instead of helpful validation messages. |
| 99 | +- **Fix**: Use `rest_framework.exceptions.ValidationError` or `django.core.exceptions.ValidationError` |
| 100 | + |
| 101 | +### 11. Pandora File Analysis Polling Issues |
| 102 | +- **File**: `testing/helpers.py:315-353` |
| 103 | +- **Issues**: |
| 104 | + - Busy-wait loop with `sleep(0.1)` for up to 500 iterations (~50s) blocks the worker thread |
| 105 | + - On exception, returns last (possibly stale) `analysis_result` |
| 106 | + - `seed_expire=0` creates permanent public links to analysis results |
| 107 | +- **Impact**: Worker thread exhaustion under load; incomplete results served as final; analysis results publicly accessible |
| 108 | +- **Fix**: Use async task queue; implement proper status callbacks; set reasonable seed expiration |
| 109 | + |
| 110 | +### 12. DKIM Check Returns Inconsistent Types |
| 111 | +- **File**: `testing/helpers.py` — `check_dkim()` function |
| 112 | +- **Impact**: Returns `(record_text, boolean_tuple)` for single selector but `{"dkim": boolean}` for multiple selectors. Callers in `views.py` expect tuple unpacking — type mismatch causes runtime `TypeError`. |
| 113 | +- **Fix**: Standardize return type to always be a dict |
| 114 | + |
| 115 | +### 13. Email Check Silent Failures |
| 116 | +- **File**: `testing/helpers.py:239-241` |
| 117 | +- **Impact**: If `checkdmarc` subprocess output is invalid JSON, result is silently set to `{}`. Users see empty results with no error indication — a "clean bill of health" when the check actually failed. |
| 118 | +- **Fix**: Detect and report parsing failures; check stderr; validate subprocess exit code |
| 119 | + |
| 120 | +### 14. Missing DMARC Upload Authentication |
| 121 | +- **File**: `testing/views.py:733-750` |
| 122 | +- **Impact**: API key passed in URL query string (exposed in logs, referrer headers, browser history). Combined with `@csrf_exempt`, this endpoint is weakly protected. |
| 123 | +- **Fix**: Move API key to `Authorization` header; add CSRF protection or proper token auth |
| 124 | + |
| 125 | +### 15. SQLite in Production |
| 126 | +- **File**: `testing_platform/settings.py:164-169` |
| 127 | +- **Impact**: SQLite cannot handle concurrent writes; "database is locked" errors under load. No connection pooling, no replication, no horizontal scaling. |
| 128 | +- **Fix**: Configure PostgreSQL for staging/production via `DATABASE_URL` env var |
| 129 | + |
| 130 | +--- |
| 131 | + |
| 132 | +## MEDIUM — Accuracy, Consistency, and Hardening |
| 133 | + |
| 134 | +### 16. API Always Returns HTTP 200 |
| 135 | +- **File**: `api/views.py:282,318,334,349,365,380` |
| 136 | +- **Impact**: All check endpoints return `200 OK` regardless of whether the check succeeded or the input was invalid. API consumers cannot distinguish success from failure via status code. |
| 137 | +- **Fix**: Return 400 for validation errors, 422 for processing failures, 200 only for successful checks |
| 138 | + |
| 139 | +### 17. IPv6 Check Logic Inconsistency |
| 140 | +- **File**: `testing/helpers.py:428-431,470-473` |
| 141 | +- **Impact**: IPv4 reachability requires TCP *or* UDP, but IPv6 reachability requires TCP *and* UDP. This asymmetry means IPv6 is incorrectly marked unreachable when only one protocol responds. |
| 142 | +- **Fix**: Use consistent logic (either protocol sufficient) for both |
| 143 | + |
| 144 | +### 18. DNS Queries Without Timeout or Error Handling |
| 145 | +- **File**: `testing/helpers.py:392,406,442` |
| 146 | +- **Impact**: `dns.query.udp()` calls without timeout parameter can hang indefinitely. Only `Timeout` and `OSError` caught — misses `DNSException`, `NXDOMAIN`, connection refused. |
| 147 | +- **Fix**: Add explicit timeouts; catch `dns.exception.DNSException` broadly |
| 148 | + |
| 149 | +### 19. TLS Version Check Fragile Nmap Parsing |
| 150 | +- **File**: `testing/helpers.py:858` |
| 151 | +- **Impact**: Assumes exact nmap script output structure (`results[k]["ciphers"]["children"]`). Different nmap versions may produce different structures — causes `KeyError` crashes. |
| 152 | +- **Fix**: Validate structure before accessing nested keys; handle missing data gracefully |
| 153 | + |
| 154 | +### 20. CSP Report Endpoint Broken |
| 155 | +- **File**: `testing/views.py:874` |
| 156 | +- **Impact**: Uses `cache.get()` and `cache.incr()` but `cache` is not imported. First CSP report received crashes with `NameError`. |
| 157 | +- **Fix**: Add `from django.core.cache import cache` import |
| 158 | + |
| 159 | +### 21. Missing Security Headers in Django Config |
| 160 | +- **File**: `testing_platform/settings.py` |
| 161 | +- **Missing**: |
| 162 | + - `SECURE_HSTS_SECONDS` (no HSTS enforcement) |
| 163 | + - `SECURE_SSL_REDIRECT` (no HTTP->HTTPS redirect) |
| 164 | + - `X_FRAME_OPTIONS = 'DENY'` (clickjacking, defaults to SAMEORIGIN) |
| 165 | + - `SECURE_CONTENT_TYPE_NOSNIFF` not explicit |
| 166 | +- **Impact**: Ironic for a security testing platform to not follow its own recommendations |
| 167 | +- **Fix**: Add all security headers; make them environment-configurable |
| 168 | + |
| 169 | +### 22. No Database Unique Constraints on Domains |
| 170 | +- **File**: `testing/models.py:9-14` |
| 171 | +- **Impact**: `Domain` model has no `unique_together` on `(user, domain)`. Users can add the same domain multiple times. Race condition in `authentication/views.py:139-175` (TOCTOU). |
| 172 | +- **Fix**: Add `unique_together = [('user', 'domain')]`; handle `IntegrityError` |
| 173 | + |
| 174 | +### 23. Cascade Delete Wipes All History |
| 175 | +- **File**: `testing/models.py:10,37,93,113,146` — All FKs use `on_delete=CASCADE` |
| 176 | +- **Impact**: Deleting a user cascades to all their domains, DMARC records, CSP endpoints, CSP reports, and test history. No soft delete, no audit trail. |
| 177 | +- **Fix**: Use `PROTECT` or `SET_NULL` for forensic data; consider soft deletes |
| 178 | + |
| 179 | +### 24. PDF Report Generation Crashes on Missing Keys |
| 180 | +- **File**: `testing/views.py:780-791` |
| 181 | +- **Impact**: Email test PDF assumes `spf`, `dmarc`, `dnssec` keys exist in results. If any check failed, keys may be missing — causing `KeyError` during PDF generation. |
| 182 | +- **Fix**: Use `.get()` with defaults for all result keys |
| 183 | + |
| 184 | +### 25. Vulnerability Lookup Without Caching or Rate Limiting |
| 185 | +- **File**: `testing/helpers.py:678-737` |
| 186 | +- **Impact**: Individual CVE lookups to external API with no caching, no batch queries, no rate limiting. API downtime causes silent failures; repeated scans re-fetch the same CVEs. |
| 187 | +- **Fix**: Cache CVE results; implement batch lookups; handle API unavailability explicitly |
| 188 | + |
| 189 | +### 26. HSTS Parsing Crashes on Malformed Headers |
| 190 | +- **File**: `testing/helpers.py:1830` |
| 191 | +- **Impact**: `int()` conversion on `max-age` value without error handling. Malformed header like `max-age=invalid` crashes the check. |
| 192 | +- **Fix**: Wrap in try/except; validate before conversion |
| 193 | + |
| 194 | +### 27. CorsMiddleware Position in Middleware Stack |
| 195 | +- **File**: `testing_platform/settings.py:127` |
| 196 | +- **Impact**: `CorsMiddleware` is last in the stack but [should be placed as high as possible](https://github.com/adamchainz/django-cors-headers#setup), ideally before `CommonMiddleware`. |
| 197 | +- **Fix**: Move to top of `MIDDLEWARE` list |
| 198 | + |
| 199 | +### 28. Admin Exposed at Default `/admin/` Path |
| 200 | +- **File**: `testing_platform/urls.py:30` |
| 201 | +- **Impact**: Django admin at predictable URL with no additional protections (IP allowlist, 2FA) |
| 202 | +- **Fix**: Use obscure admin URL; add IP restrictions; require 2FA |
| 203 | + |
| 204 | +--- |
| 205 | + |
| 206 | +## LOW — Technical Debt and Improvements |
| 207 | + |
| 208 | +### 29. Monolithic helpers.py (~1900 Lines, ~41 Functions) |
| 209 | +- Single file containing all security check logic — DNS, TLS, IPv6, email, web server, CSP, cookies, CORS, HSTS, SRI, etc. |
| 210 | +- Hard to test individual functions; changes risk regressions; no separation of concerns |
| 211 | +- **Suggestion**: Split into domain-specific modules (`checks/dns.py`, `checks/tls.py`, `checks/headers.py`, etc.) |
| 212 | + |
| 213 | +### 30. No Async Task Queue |
| 214 | +- All checks (nmap, Pandora, DNS) run synchronously in request/response cycle |
| 215 | +- Long-running scans block gunicorn worker threads |
| 216 | +- No task status tracking, no retry mechanism, no progress reporting |
| 217 | +- **Suggestion**: Integrate Celery or Django-Q for background processing |
| 218 | + |
| 219 | +### 31. Inconsistent Error Return Formats |
| 220 | +- Some functions return `{"error": "msg"}`, others `{"status": False}`, others `{"result": {"error": "msg"}}` |
| 221 | +- Views and API must handle multiple formats; no contract between helpers and consumers |
| 222 | +- **Suggestion**: Define a standard result envelope (`{success, data, error}`) |
| 223 | + |
| 224 | +### 32. Mixed Authentication Patterns |
| 225 | +- Some API views use 3 auth methods (Session + Basic + JWT), some use JWT only, some are fully public |
| 226 | +- No consistent pattern; no documentation of which endpoints need what |
| 227 | +- **Suggestion**: Define auth tiers in code and docs |
| 228 | + |
| 229 | +### 33. SRI Check Fetches External Resources Without Circuit Breaker |
| 230 | +- **File**: `testing/helpers.py:1547-1632` |
| 231 | +- Fetches every cross-origin resource; slow CDNs make the check hang |
| 232 | +- Uses `print()` instead of `logging` |
| 233 | +- **Suggestion**: Add circuit breaker pattern; use proper logging |
| 234 | + |
| 235 | +### 34. checkdmarc Dependency Not Validated at Startup |
| 236 | +- **File**: `testing/helpers.py:227-244` |
| 237 | +- Calls system binary via subprocess; no check that it's installed or correct version |
| 238 | +- **Suggestion**: Validate external tool availability at startup (`AppConfig.ready()`) |
| 239 | + |
| 240 | +### 35. CORS Hardcoded for localhost |
| 241 | +- **File**: `testing_platform/settings.py:129-132` |
| 242 | +- Only allows `localhost:3000` — works for dev but needs environment-based config for production |
| 243 | +- **Suggestion**: Make `CORS_ALLOWED_ORIGINS` configurable via env var |
| 244 | + |
| 245 | +--- |
| 246 | + |
| 247 | +## Summary by Category |
| 248 | + |
| 249 | +| Category | Critical | High | Medium | Low | |
| 250 | +|----------|----------|------|--------|-----| |
| 251 | +| Authentication & Authorization | 2 | 1 | 1 | 1 | |
| 252 | +| Input Validation | 1 | 2 | 2 | 0 | |
| 253 | +| CSRF / Cookie Security | 2 | 1 | 0 | 0 | |
| 254 | +| Feature Accuracy | 0 | 3 | 5 | 2 | |
| 255 | +| Configuration & Deployment | 1 | 1 | 2 | 1 | |
| 256 | +| API Design | 0 | 1 | 1 | 2 | |
| 257 | +| Data Integrity | 0 | 0 | 2 | 1 | |
| 258 | +| **Total** | **6** | **9** | **13** | **7** | |
| 259 | + |
| 260 | +## Recommended Fix Order |
| 261 | + |
| 262 | +1. **Immediate**: #1 (credentials), #2 (unauth endpoints), #3 (file size), #5 (defaults) |
| 263 | +2. **Week 1**: #4+#6 (CSRF/cookies), #7 (validation bypass), #8 (rate limiting), #10 (validators) |
| 264 | +3. **Week 2**: #9 (nmap timeout), #11 (Pandora), #13 (silent failures), #14 (DMARC auth), #15 (database) |
| 265 | +4. **Week 3**: #16-#21 (accuracy fixes, headers, broken endpoints) |
| 266 | +5. **Month 1**: #22-#28 (hardening, data integrity) |
| 267 | +6. **Ongoing**: #29-#35 (architecture, async, tech debt) |
0 commit comments