diff --git a/security/pentest-2026-06-12.md b/security/pentest-2026-06-12.md new file mode 100644 index 0000000..21b8ae9 --- /dev/null +++ b/security/pentest-2026-06-12.md @@ -0,0 +1,95 @@ +# Pentest mini-report — lima-app — 2026-06-12 + +**Probed URL:** https://limaimpro.duckdns.org/ +**Stack:** React 18 + Vite 7 (SPA/PWA) / npm / Python FastAPI backend / PWA=yes +**Counts:** Critical=1 High=5 Medium=5 Low=3 Info=2 + +## Findings + +| Sev | Cat | Title | Location | +|---|---|---|---------| +| Critical | SCA | vitest <3.2.6: arbitrary file read/exec | package.json | +| High | SAST | Hardcoded seed admin credentials, no production guard | backend/app/main.py:53-121 | +| High | SAST | JWT stored in sessionStorage (unconditional, not Safari-only) | src/lib/api.ts:44 | +| High | SCA | serialize-javascript RCE via RegExp.flags (override insufficient) | node_modules/serialize-javascript@6.0.2 | +| High | SCA | fast-uri path traversal + host confusion (override insufficient) | node_modules/fast-uri@3.1.0 | +| High | SCA | @babel/plugin-transform-modules-systemjs arbitrary code gen | indirect via workbox-build | +| Medium | SAST | Unauthenticated /health/db leaks DB URL prefix + table names | backend/app/main.py:202 | +| Medium | SAST | DEFAULT_JWT_SECRET hardcoded in source | backend/app/config.py:10 | +| Medium | SAST | CORS defaults include localhost origins in production | backend/app/config.py:15 | +| Medium | Infra | CSP style-src 'unsafe-inline' | nginx.conf:20 | +| Medium | SCA | react-router 6.30.3 open redirect via // prefix | react-router@6.30.3 | +| Medium | SCA | postcss 8.5.9 XSS via in CSS stringify | postcss@8.5.9 | +| Low | Infra | HSTS missing preload directive | nginx.conf:21 | +| Low | SAST | dangerouslySetInnerHTML in ChartStyle (static data) | src/components/ui/chart.tsx:70 | +| Low | SAST | 8-hour access token window widens session hijack exposure | backend/app/config.py:33 | +| Info | Infra | API /docs disabled in production | backend/app/main.py:146 | +| Info | PWA | SW NetworkFirst caching may serve stale data offline | vite.config.ts:76 | + +## Top 3 fixes + +1. **Hardcoded seed credentials** — Add `if not settings.is_development: return` as the first line of `_ensure_seed_data()` so demo accounts are never seeded in production; rotate any accounts already created. +2. **vitest critical CVE** — Run `npm install vitest@^3.2.6`; CVSS 9.8 path traversal/RCE affects the dev/CI environment. +3. **JWT in sessionStorage** — Gate `setSessionToken()` behind a Safari/ITP detection check; rely on httpOnly cookies for all other browsers to prevent XSS token theft. + +## Evidence (Critical/High only) + +### [Critical] vitest <3.2.6 — GHSA-5xrq-8626-4rwp +- **Location:** `node_modules/vitest@3.2.4` (devDependency) +- **Evidence:** CVSS 9.8 — "When Vitest UI server is listening, arbitrary file can be read and executed" (CWE-862) +- **Impact:** Attacker with network access to the Vitest UI port can read any file on the CI/dev host and execute code. +- **Fix:** `npm install vitest@^3.2.6` + +### [High] Hardcoded seed admin credentials — no production guard +- **Location:** `backend/app/main.py:53-62` + `main.py:129-135` +- **Evidence:** `{"email": "admin@lima-impro.fr", ..., "password": "Admin1234!", "app_role": "admin"}` +- **Impact:** On a fresh production DB, an admin account with a publicly-known password is created automatically at first boot — full application compromise. +- **Fix:** Add `if not settings.is_development: return` as the first line of `_ensure_seed_data()`. + +### [High] JWT access token in sessionStorage (unconditional) +- **Location:** `src/lib/api.ts:44-45`, `src/contexts/AuthContext.tsx:76-77` +- **Evidence:** `setSessionToken(res.access_token)` called for every login; `sessionStorage.setItem(_SESSION_KEY, token)` +- **Impact:** Any XSS payload can read `sessionStorage` and exfiltrate the raw access token to authenticate as the victim. +- **Fix:** Only call `setSessionToken()` when httpOnly cookies are blocked (Safari ITP detection); rely on httpOnly cookie flow for all other browsers. + +### [High] serialize-javascript RCE — GHSA-5c6j-r48x-rmvq +- **Location:** `node_modules/serialize-javascript@6.0.2` (via @rollup/plugin-terser → workbox-build) +- **Evidence:** Override `"serialize-javascript": ">=6.0.2"` resolves to 6.0.2 which is in vulnerable range `<=7.0.4`; fix requires `>=7.0.5`. +- **Impact:** Crafted input to the build pipeline can trigger RCE during `npm run build`. +- **Fix:** Change override to `"serialize-javascript": ">=7.0.5"` and run `npm install`. + +### [High] fast-uri path traversal + host confusion — GHSA-q3j6-qgpj-74h6 / GHSA-v39h-62p7-jpjc +- **Location:** `node_modules/fast-uri@3.1.0` +- **Evidence:** Override `"fast-uri": ">=3.1.0"` installs 3.1.0 which is `<=3.1.1` (vulnerable); fix requires `>=3.1.2`. +- **Impact:** Percent-encoded dot segments can bypass path validation; host confusion via encoded authority delimiters. +- **Fix:** Change override to `"fast-uri": ">=3.1.2"` and run `npm install`. + +### [High] @babel/plugin-transform-modules-systemjs — GHSA-fv7c-fp4j-7gwp +- **Location:** Indirect dependency via workbox-build +- **Evidence:** CVSS 8.2 — "generates arbitrary code when compiling malicious input" (CWE-94, CWE-843); current override `>=7.27.0` remains in vulnerable range 7.12.0–7.29.3. +- **Impact:** Malicious input to the build pipeline can cause arbitrary code execution at build time. +- **Fix:** Override `"@babel/plugin-transform-modules-systemjs": ">=7.30.0"` and run `npm install`. + +## Verified safe + +- `.env`, `.env.local`, `.git/config`, `.git/HEAD`, `backup.zip`, `dump.sql` — all return HTTP 403 (Traefik + Nginx blocking) +- TLS 1.0 and TLS 1.1 are disabled; only TLS 1.2 and 1.3 accepted +- TLS certificate is valid (Let's Encrypt, auto-renewed, ~30 days remaining) +- API `/docs` and `/redoc` are disabled in production via FastAPI config +- All `navigate()` calls use hardcoded string paths — no user-controlled redirect targets found +- `dangerouslySetInnerHTML` in `chart.tsx` is populated only from typed developer config, not user data +- No raw secrets found in tracked source files or `.env` files in repo +- JWT validation enforces `type: "access"` / `type: "refresh"` claim separation +- Login endpoint has `@limiter.limit("5/minute")` rate limiting +- `DEFAULT_JWT_SECRET` is rejected at startup for non-development environments via `validate_jwt_secret` + +## Needs server-side verification + +- Whether `CORS_ORIGINS` is explicitly set in the Railway production environment (if not, localhost origins are allowed) +- Whether `_ensure_seed_data()` already ran in production (check if `admin@lima-impro.fr` exists in the production DB) +- Whether the Railway API host `/health/db` endpoint is network-accessible without authentication +- HSTS preload eligibility for `limaimpro.duckdns.org` (DuckDNS subdomains may be ineligible) + +## Tools +ran=npm-audit, grep-secrets, curl-headers(blocked-403), curl-sensitive-paths, openssl-tls, grep-SAST-innerHTML/eval/postMessage/localStorage/JWT/prototype-pollution, manual-code-review(nginx.conf, vite.config.ts, api.ts, AuthContext.tsx, main.py, config.py, security.py, auth.py, chart.tsx) +skipped=dynamic-api-probe(Railway-API-blocked-by-sandbox-egress), pip-audit(Python-not-installed)