diff --git a/security/pentest-2026-06-09.md b/security/pentest-2026-06-09.md new file mode 100644 index 0000000..81e2dc5 --- /dev/null +++ b/security/pentest-2026-06-09.md @@ -0,0 +1,71 @@ +# Pentest mini-report — versila22/lima-app — 2026-06-09 + +**Probed URL:** https://limaimpro.duckdns.org/ +**Stack:** React 18 + Vite / npm / PWA=yes (vite-plugin-pwa, workbox, autoUpdate) +**Counts:** Critical=0 High=3 Medium=4 Low=3 Info=2 + +## Findings + +| Sev | Cat | Title | Location | +|---|---|---|---| +| High | SAST | JWT token stored in sessionStorage (Safari fallback) | `src/lib/api.ts:42-53` | +| High | SAST | `.orig` backup file with localStorage token storage committed to git | `src/lib/api.ts.orig:22-33` | +| High | Infra | react-router-dom open redirect via `//`-prefixed path (CVE GHSA-2j2x-hqr9-3h42) | `node_modules/react-router` v6 <6.30.4 | +| Medium | SAST | `dangerouslySetInnerHTML` injects CSS from chart config into DOM | `src/components/ui/chart.tsx:70` | +| Medium | PWA | Service worker caches all JS/CSS/HTML including auth routes (NetworkFirst for API, but SW intercepts) | `vite.config.ts` workbox globPatterns | +| Medium | Infra | CSP `style-src 'unsafe-inline'` weakens XSS mitigation | `nginx.conf:18` | +| Medium | Infra | Dependency audit: serialize-javascript RCE in build toolchain (GHSA-5c6j-r48x-rmvq) | `node_modules/serialize-javascript` (workbox-build dep) | +| Low | SAST | Sidebar state cookie set without `Secure` or `SameSite` flags | `src/components/ui/sidebar.tsx:68` | +| Low | Infra | `X-Powered-By` / `Server` header absent (good), but no `Cache-Control: no-store` on auth API responses — needs server-side check | API backend | +| Low | Infra | `.env.production` and `.env.development` committed to git (contain VITE_API_URL only — no secrets currently, but sets bad precedent) | `.env.production`, `.env.development` | +| Info | Infra | TLS 1.0/1.1 disabled, TLS 1.2+1.3 enabled, cert valid until 2026-07-09 | limaimpro.duckdns.org:443 | +| Info | Infra | vitest critical CVE (GHSA-5xrq-8626-4rwp) — devDependency only, not deployed | `package.json` devDependencies | + +## Top 3 fixes +1. **react-router open redirect** — Upgrade `react-router-dom` to ≥6.30.4 (`npm install react-router-dom@latest`). +2. **sessionStorage JWT fallback** — Evaluate removing the Safari fallback or scope it strictly; rotate tokens frequently and document the accepted risk. +3. **`.orig` file in git** — `git rm src/lib/api.ts.orig` and purge from history with `git filter-repo` to remove the old localStorage token pattern. + +## Evidence (Critical/High only) + +**H1 — JWT in sessionStorage** +- Location: `src/lib/api.ts:42-53`, `src/contexts/AuthContext.tsx:73-76` +- Snippet: `const _SESSION_KEY = "lima_access_token"; sessionStorage.setItem(_SESSION_KEY, token);` +- Impact: Any same-origin JS (XSS, malicious extension) can steal the access token; XSS + sessionStorage = full auth compromise. +- Fix: Prefer httpOnly cookies exclusively; remove sessionStorage fallback or gate it behind user-visible Safari warning. + +**H2 — `.orig` backup with localStorage token pattern committed** +- Location: `src/lib/api.ts.orig:22-33` (tracked by git: `git ls-files` confirms) +- Snippet: `const TOKEN_KEY = "lima_token"; localStorage.getItem(TOKEN_KEY); localStorage.setItem(TOKEN_KEY, token);` +- Impact: Old insecure pattern (localStorage instead of httpOnly cookie) persists in repo history; token more persistent and broader-scope than sessionStorage. +- Fix: `git rm src/lib/api.ts.orig && git filter-repo --path src/lib/api.ts.orig --invert-paths` + +**H3 — react-router open redirect** +- Location: `node_modules/react-router` <6.30.4 (GHSA-2j2x-hqr9-3h42) +- Snippet: `` or `useNavigate()("//attacker.example")` → browser reinterprets as protocol-relative URL. +- Impact: Phishing redirect after login; attacker can construct login links that redirect users to a malicious site. +- Fix: `npm install react-router-dom@latest` (≥6.30.4). + +## Verified safe +- No hardcoded secrets in committed files (`.env.production` contains only public VITE_API_URL, not keys) +- CORS: explicit allowlist (`limaimpro.duckdns.org`, localhost variants) with `allow_credentials=True` — no wildcard reflection +- HSTS enabled (`max-age=31536000; includeSubDomains`) on both nginx and backend middleware +- `X-Frame-Options: DENY` and `frame-ancestors 'none'` in CSP — clickjacking protected +- `X-Content-Type-Options: nosniff` present +- TLS 1.0/1.1 disabled; TLS 1.2 and 1.3 only +- Sensitive file probes (.env, .git/config, backup.zip) all return 403 +- `dangerouslySetInnerHTML` in chart.tsx uses only hardcoded dev-time config (CSS color strings), not server data +- JWT_SECRET weak-default check enforced at startup in non-dev mode (`validate_jwt_secret` validator) +- No VAPID private key in client bundle (no push notifications configured) +- No `eval()` or `document.write()` in source +- react-markdown used without `rehype-raw` — no HTML passthrough risk + +## Needs server-side verification +- Cookie flags (`HttpOnly`, `Secure`, `SameSite=Strict/Lax`) on auth cookies set by FastAPI backend (Railway-hosted, not audited here) +- `Cache-Control: no-store` on `/auth/*` and `/api/*` responses from backend +- Rate-limiting effectiveness on `/auth/login` (slowapi present in backend, thresholds not reviewed) +- Backend CSP header values (SecurityHeadersMiddleware sets `default-src 'none'` for API — verify in prod response) +- Sentry DSN exposure: `VITE_SENTRY_DSN` baked into bundle at build time — confirm it is intentionally public or rotate if leaked + +## Tools +ran=npm-audit, curl-headers, curl-cors, curl-sensitive-file-probe, openssl-tls; skipped=none