diff --git a/security/pentest-2026-06-10.md b/security/pentest-2026-06-10.md new file mode 100644 index 0000000..e15df88 --- /dev/null +++ b/security/pentest-2026-06-10.md @@ -0,0 +1,91 @@ +# Pentest mini-report — versila22/lima-app — 2026-06-10 + +**Probed URL:** https://limaimpro.duckdns.org/ +**Stack:** React 18 + Vite 7 / npm (bun.lock also present) / PWA=yes (vite-plugin-pwa workbox, autoUpdate) +**Counts:** Critical=2 High=4 Medium=7 Low=5 Info=0 + +## Findings + +| Sev | Cat | Title | Location | +|---|---|---|---| +| Critical | SAST | Hardcoded Cloudflare R2 credentials in git history | backend/scripts/_archive/migrate_local_to_r2.py:11-12 | +| Critical | SAST | vitest CVSS 9.8 — arbitrary file read/exec when UI server running (GHSA-5xrq-8626-4rwp) | package.json (vitest ~3.2.4) | +| High | SAST | JWT access token stored in sessionStorage (XSS-accessible) — Safari cross-origin fallback | src/lib/api.ts:44-53 | +| High | SAST | Seed data contains hardcoded default passwords (Admin1234!, Password1!) | backend/app/main.py:54-62 | +| High | DAST | react-router open redirect via `//` prefix (GHSA-2j2x-hqr9-3h42) — react-router-dom ~6.30.1 | package.json | +| High | SAST | X-Forwarded-For not trusted-proxy-validated — rate limit bypass via header spoofing | backend/app/limiting.py:14-16 | +| Medium | DAST | `/health/migrations` leaks traceback + DB URL prefix, unauthenticated | backend/app/main.py:224-238 | +| Medium | Infra | nginx `/assets/` block drops all security headers (add_header inheritance gap) | nginx.conf | +| Medium | DAST | SameSite=None cookies — CSRF protection disabled, relies solely on CORS | backend/app/utils/security.py:107-129 | +| Medium | SAST | No max-size on photo data-URI upload — potential DB bloat / DoS | backend/app/routers/members.py:336-356 | +| Medium | SAST | Archived script reset commission accounts to weak single-word passwords | backend/scripts/_archive/reset_passwords.py:10-16 | +| Medium | Infra | HSTS missing `preload` directive | nginx.conf | +| Medium | Infra | Let's Encrypt cert expires 2026-07-10 — verify auto-renewal is monitored | limaimpro.duckdns.org:443 | +| Low | SAST | `api.ts.orig` (localStorage token storage) committed to repo | src/lib/api.ts.orig | +| Low | SAST | CORS default origin includes `lovable.app` — unintended cross-origin credential acceptance | backend/app/config.py:37 | +| Low | SAST | react-markdown used without html sanitization — safe now; guard against future rehype-raw | src/ | +| Low | Infra | Sentry DSN baked into bundle (VITE_SENTRY_DSN) — fake event injection possible | vite.config.ts | +| Low | Infra | `img-src https:` in CSP is overly permissive (wildcard HTTPS origin) | nginx.conf | + +## Top 3 fixes +1. **Rotate R2 credentials** — Revoke `85ac3f95ea5bb38cb2a4575909d12cae` in Cloudflare dashboard immediately; purge from git history with `git filter-repo` +2. **Update vulnerable deps** — `npm update vitest react-router-dom` (vitest ≥3.2.6, react-router-dom ≥6.30.4) +3. **Fix rate limiter trusted proxy** — Use `X-Real-IP` or last IP in `X-Forwarded-For` chain rather than first (attacker-controlled) value + +## Evidence (Critical/High only) + +**C1 — R2 credentials in source** +- Location: `backend/scripts/_archive/migrate_local_to_r2.py:11-12` +- Snippet: `aws_access_key_id="85ac3f95ea5bb38cb2a4575909d12cae", aws_secret_access_key="efaf5aab9b029..."` +- Impact: Full read/write access to R2 bucket (member profile photos) if repo is leaked or VPS is compromised +- Fix: Rotate creds in Cloudflare dashboard; run `git filter-repo --path backend/scripts/_archive/migrate_local_to_r2.py --invert-paths` and force-push all remotes + +**C2 — vitest GHSA-5xrq-8626-4rwp (CVSS 9.8)** +- Location: `package.json` — vitest ~3.2.4 (fix: ≥3.2.6) +- Snippet: `"vitest": "^3.2.4"` — missing auth check allows arbitrary file read/exec when UI server is listening +- Impact: Unauthenticated remote code execution on any machine running `vitest --ui` (CI runners, developer machines) +- Fix: `npm update vitest` + +**H1 — JWT in sessionStorage** +- Location: `src/lib/api.ts:44-53`, `src/contexts/AuthContext.tsx:74-77` +- Snippet: `sessionStorage.setItem(_SESSION_KEY, token)` — bearer token stored as Safari cross-origin fallback +- Impact: Any XSS on the page steals the token → full account takeover; httpOnly cookie path is safe +- Fix: Evaluate if Safari fallback is still needed; if yes, tighten CSP and use short TTL + server-side revocation on logout + +**H2 — Hardcoded seed passwords** +- Location: `backend/app/main.py:54-62` +- Snippet: `"password": "Admin1234!"` (admin), `"password": "Password1!"` (8 members) +- Impact: If production DB was seeded and passwords never changed, admin account is trivially compromised +- Fix: Audit all production seed accounts; remove passwords from seed data; use activation flow or random first-run passwords + +**H3 — react-router open redirect (GHSA-2j2x-hqr9-3h42)** +- Location: `package.json` — react-router-dom ~6.30.1 (fix: ≥6.30.4) +- Snippet: Redirect to `//attacker.example` path treated as protocol-relative URL by affected versions +- Impact: Phishing via crafted `?next=//attacker.example` links if any app flow reads redirect target from query params +- Fix: `npm update react-router-dom` + +**H4 — Rate limit X-Forwarded-For spoofing** +- Location: `backend/app/limiting.py:14-16` +- Snippet: `X-Forwarded-For` read without trusted-proxy validation (first value is attacker-controlled) +- Impact: Bypass per-IP rate limits on login (5/min) and forgot-password (3/min) → enables credential stuffing +- Fix: Trust only the IP injected by Railway's proxy; use `X-Real-IP` or last hop in `X-Forwarded-For` + +## Verified safe +- No XSS sinks (dangerouslySetInnerHTML, innerHTML=, eval, document.write) in src/ +- No postMessage origin confusion found +- No prototype pollution surface (no lodash, no unvalidated spread on user input) +- TLS 1.0/1.1 disabled; TLS 1.2 + 1.3 enabled; cert valid until 2026-07-10 +- Sensitive files (.env, .git/config, etc.) all return 403 +- CORS does not reflect attacker origin with credentials +- Nginx CSP, X-Frame-Options, X-Content-Type-Options, HSTS present in main server block +- httpOnly cookies used as primary auth mechanism (sessionStorage only as Safari fallback) + +## Needs server-side verification +- Whether production DB admin/seed accounts have had default passwords changed +- Whether commission account passwords (ca, spectacle, comprog, comcom, comform) are still set to weak values +- Whether Railway proxy sets `X-Real-IP` reliably (affects H4 fix approach) +- Whether Traefik auto-renewal is active and monitored for the Let's Encrypt cert +- Whether Vitest UI server is ever run in CI or exposed to network (affects C2 exploitability) + +## Tools +ran=npm-audit, grep-secrets, curl-headers, curl-cors, curl-sensitive-files, openssl-tls; skipped=none