diff --git a/security/pentest-2026-06-20.md b/security/pentest-2026-06-20.md new file mode 100644 index 0000000..219dbff --- /dev/null +++ b/security/pentest-2026-06-20.md @@ -0,0 +1,78 @@ +# Pentest mini-report — versila22/lima-app — 2026-06-20 + +**Probed URL:** https://limaimpro.duckdns.org/ +**Stack:** React 18 + Vite 7 SPA / npm / PWA=yes (vite-plugin-pwa, Workbox NetworkFirst for API) +**Counts:** Critical=1 High=4 Medium=1 Low=3 Info=0 + +> Note: deployed URL returned 403 from audit host (network policy). DAST headers verified from nginx.conf source. TLS checked via openssl (TLS 1.0/1.1 disabled, TLS 1.2+1.3 enabled). Exposed-path checks skipped. + +## Findings + +| Sev | Cat | Title | Location | +|---|---|---|---| +| Critical | Dep | vitest: arbitrary file read+exec via UI server | package.json → vitest (dev) | +| High | Dep | serialize-javascript: RCE via RegExp.flags | package.json → workbox-build → serialize-javascript (dev) | +| High | Dep | fast-uri: path traversal via %2E segments | package.json → vite → fast-uri (dev) | +| High | Dep | undici: TLS cert bypass via SOCKS5 requestTls drop | package.json → vite → undici (dev) | +| High | Dep | vite: NTLMv2 hash disclosure via UNC path (Windows) | package.json → vite (dev) | +| Medium | Dep | react-router-dom: open redirect via `//`-prefixed path | package.json → react-router-dom ^6.30.1 (runtime) | +| Low | DAST | CSP style-src unsafe-inline | nginx.conf:10 | +| Low | SAST | api.ts.orig committed — historical localStorage token pattern | src/lib/api.ts.orig | +| Low | SAST | api_upload.patch committed — stale patch artifact | src/lib/api_upload.patch | + +## Top 3 fixes +1. **react-router-dom open redirect** — `npm update react-router-dom` to a patched release (>=6.x fixing GHSA-jkqr-q8mg-9736) +2. **Dev dependency chain (Critical/High)** — `npm update vitest vite workbox-build` to resolve CVE chain; add `"vitest": ">=X.Y.Z"` overrides in package.json +3. **Stale artifacts in repo** — `git rm src/lib/api.ts.orig src/lib/api_upload.patch` to prevent confusion and accidental regression to insecure localStorage approach + +## Evidence (Critical/High only) + +**Critical — vitest arbitrary file read/exec (dev)** +- Location: `package.json` devDependencies → `vitest ^3.2.4` (installed <3.x patched ver) +- Snippet: `"vitest": "^3.2.4"` — GHSA-9crc-q9x8-hgqq +- Impact: Attacker with access to the Vitest UI dev server can read arbitrary files and execute code on the dev machine +- Fix: Update vitest; never expose port 51204 or `--ui` flag outside localhost + +**High — serialize-javascript RCE (dev/build)** +- Location: `package.json` → `workbox-build` → `@rollup/plugin-terser` → `serialize-javascript` +- Snippet: `"workbox-build"` in devDeps — GHSA-h4j5-g7hw-2h49 +- Impact: Malicious regex in build input can achieve RCE during `vite build` +- Fix: `npm update workbox-build` or pin `"serialize-javascript": ">=6.0.2"` in overrides (already present — verify lockfile resolves it) + +**High — fast-uri path traversal (dev)** +- Location: `package.json` → `vite` → `fast-uri` — GHSA-vc7q-jjfg-4q4f +- Impact: Vite dev server file-serve bypass via %2E-encoded dotdot segments +- Fix: Update vite to version bundling patched fast-uri; pin `"fast-uri": ">=3.1.0"` in overrides (already present) + +**High — undici TLS bypass (dev)** +- Location: `package.json` → `vite/undici` — GHSA-3xgq-xp9f-89fc +- Impact: Vite dev server's fetch/proxy drops TLS options for SOCKS5 proxy connections, bypassing certificate validation +- Fix: Update undici / vite + +**High — vite NTLMv2 hash disclosure (dev, Windows)** +- Location: `package.json` → `vite` via `launch-editor` — GHSA-8wh2-6qhj-h7j9 +- Impact: Visiting a crafted URL during vite `--open` triggers a UNC path, disclosing NTLMv2 hashes to a remote SMB host +- Fix: Update vite >=6.x with patched launch-editor + +## Verified safe +- No hardcoded secrets or API keys in source (secret grep clean) +- JWT token: migrated from localStorage to sessionStorage (httpOnly-cookie primary, Safari fallback) — XSS impact limited to tab session +- PWA: no VAPID_PRIVATE in client; runtime API caching uses NetworkFirst (not stale-while-revalidate on auth routes); SW scope / +- Backend CORS: explicit allow_origins list; `allow_credentials=True` only with named origins, never wildcard +- Backend security headers middleware: HSTS, nosniff, X-Frame-Options: DENY, CSP (API-only), COOP, CORP all set +- Nginx CSP: script-src 'self' (no unsafe-eval, no unsafe-inline for scripts) +- Nginx: HSTS max-age=31536000 + includeSubDomains, X-Frame-Options: DENY, nosniff set +- No postMessage usage +- No prototype pollution surface (no lodash.merge/spread on user input) +- dangerouslySetInnerHTML in chart.tsx: CSS variables built from developer-controlled ChartConfig, not user input +- TLS: 1.0/1.1 disabled; 1.2+1.3 enabled +- JWT default secret blocked in non-dev mode via pydantic model_validator + +## Needs server-side verification +- Deployed response headers (curl returned 403 from audit host) — confirm CSP, HSTS, nosniff present on live limaimpro.duckdns.org +- Cookie flags (Secure, HttpOnly, SameSite) on auth cookies from Railway backend +- CORS reflection test on live API (api-production-e15b.up.railway.app) +- Exposed paths check: .env, .git/config, backup.zip on deployed URL + +## Tools +ran=npm-audit, grep-secret-scan, openssl-tls; skipped=curl-dast (network policy 403), nmap