From c6b155520c40f888d27b260185782a8c0afe148a Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Sat, 28 Mar 2026 21:17:48 +0100 Subject: [PATCH] docs: add security hardening plan from 360 audit 18 prioritized findings (6 HIGH, 6 MEDIUM, 6 LOW) filtered against what is already deployed on Hetzner. Covers app-layer issues like login rate limiting, GraphQL injection, SSRF, GDPR/Sentry PII, dependency pinning, and CSP headers. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/proposals/security-hardening-plan.md | 200 ++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 docs/proposals/security-hardening-plan.md diff --git a/docs/proposals/security-hardening-plan.md b/docs/proposals/security-hardening-plan.md new file mode 100644 index 00000000..71176d39 --- /dev/null +++ b/docs/proposals/security-hardening-plan.md @@ -0,0 +1,200 @@ +# Security Hardening Plan + +Findings from a 360 security audit (2026-03-28), filtered against what is already deployed on Hetzner (Steps 1-25 in `docs/deployment/hetzner-server-setup.md`). + +Items are ordered by priority. Each will be tackled individually upon prompt. + +--- + +## HIGH Priority + +### 1. Rate-limit login endpoints at app level + +**Problem:** Cloudflare rate-limits `/api/` broadly at 100/10s, but login endpoints (`/api/v1/auth/login`, forgot-password, resend-verification) need much stricter limits. If Cloudflare is bypassed via direct IP or an attacker is behind CF, brute-force is possible. + +**Files:** +- `app/modules/tenancy/routes/api/admin_auth.py` +- `app/modules/tenancy/routes/api/merchant_auth.py` +- `app/modules/tenancy/routes/api/store_auth.py` +- `app/modules/customers/routes/api/storefront.py` + +**Fix:** Apply `@rate_limit(max_requests=5, window_seconds=300)` to all login and `@rate_limit(max_requests=3, window_seconds=3600)` to password reset/resend-verification endpoints. + +--- + +### 2. GraphQL template injection in Letzshop client + +**Problem:** `QUERY_SHIPMENTS_PAGINATED_TEMPLATE.format(state=state)` injects the `state` parameter directly into the GraphQL query string instead of using GraphQL variables. + +**Files:** +- `app/modules/marketplace/services/letzshop/client_service.py:798` +- `scripts/letzshop_introspect.py:487` +- `scripts/check_letzshop_shipment.py:97` + +**Fix:** Refactor to use the `variables` parameter in the GraphQL request payload. + +--- + +### 3. SSRF in CSV processor + +**Problem:** `requests.get(url, timeout=30)` accepts any URL with no domain whitelist or internal IP blocking. An admin could target internal Docker network IPs (172.x.x.x, 10.x.x.x) or cloud metadata endpoints. + +**File:** `app/utils/csv_processor.py:102-106` + +**Fix:** Add domain whitelist and block RFC1918/link-local IP ranges before making the request. + +--- + +### 4. Sentry PII exposure (GDPR) + +**Problem:** `send_default_pii=True` sends user emails and IP addresses to Sentry (US-hosted SaaS). For EU-based SaaS with .lu domains, this needs GDPR consideration. + +**Files:** +- `main.py:52` +- `app/core/celery_config.py` (same setting) + +**Fix:** Set `send_default_pii=False` and configure Sentry data scrubbing rules, or document GDPR basis for the data transfer. + +--- + +### 5. Fail-fast on insecure production defaults + +**Problem:** `validate_production_settings()` in `app/core/config.py:314-337` prints warnings but allows the app to start with `admin123`, placeholder JWT secret, wildcard CORS, or debug mode in production. + +**File:** `app/core/config.py:314-337` + +**Fix:** Raise `SystemExit` if any critical misconfiguration is detected when `APP_ENV=production`. + +--- + +### 6. JWT secret fallback in code + +**Problem:** `middleware/auth.py:69-70` silently falls back to `"your-secret-key-change-in-production-please"` if `JWT_SECRET_KEY` env var is missing. Production is properly configured (Step 10), but the code should not have a weak default. + +**File:** `middleware/auth.py:69-70` + +**Fix:** Raise an error if `JWT_SECRET_KEY` is not set (remove the default fallback). + +--- + +## MEDIUM Priority + +### 7. Pin critical unpinned dependencies + +**Problem:** Several security-sensitive packages use `>=` instead of exact pinning: `PyJWT>=2.0.0`, `google-auth>=2.0.0`, `Pillow>=10.0.0`, `boto3>=1.34.0`, `sentry-sdk>=2.0.0`. + +**File:** `requirements.txt` + +**Fix:** Pin to exact versions. Run `pip freeze` to capture current working versions. + +--- + +### 8. Pin Docker image tags + +**Problem:** Monitoring stack images use `:latest` tag: Prometheus, Grafana, cAdvisor, Alertmanager, Redis Exporter, Gitea. + +**Files:** +- `docker-compose.yml` +- `~/gitea/docker-compose.yml` (on server) + +**Fix:** Pin to specific version tags (e.g., `prom/prometheus:v2.51.0`). + +--- + +### 9. Replace `random` with `secrets` for order numbers + +**Problem:** `random.choices()` and `random.randint()` are not cryptographically secure. Order numbers and store code suffixes should be unpredictable. + +**Files:** +- `app/modules/orders/services/order_service.py:70-79` +- `app/modules/marketplace/services/letzshop/store_sync_service.py:436, 464, 469` + +**Fix:** Replace with `secrets.choice()` and `secrets.randbelow()`. + +--- + +### 10. Switch rate limiter to Redis backend + +**Problem:** In-memory `defaultdict(deque)` rate limiter doesn't persist across restarts and won't work with multiple Gunicorn workers. + +**File:** `middleware/rate_limiter.py` + +**Fix:** Use Redis (already in the stack) as the backing store for rate limit counters. Not urgent while running a single worker, but needed before horizontal scaling. + +--- + +### 11. Add Content-Security-Policy header + +**Problem:** Security headers middleware sets HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy — but no CSP. Cloudflare doesn't add one either. + +**File:** `middleware/security_headers.py` + +**Fix:** Add a restrictive CSP header. Start with `default-src 'self'` and add exceptions for CDN assets, inline scripts (Alpine.js), etc. + +--- + +### 12. Validate file download paths + +**Problem:** `get_file_content(file_path)` reads arbitrary paths without verifying the path is under the expected upload directory. + +**File:** `app/modules/messaging/services/message_attachment_service.py:213-222` + +**Fix:** Resolve path with `Path.resolve()` and verify it starts with the expected base directory. + +--- + +## LOW Priority + +### 13. SVG upload sanitization + +**Problem:** SVG files are allowed in media uploads. SVGs can contain embedded JavaScript (XSS vector). + +**File:** `app/modules/cms/services/media_service.py` + +**Fix:** Either disallow SVG uploads or sanitize with `defusedxml` / strip `