Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions security/pentest-2026-06-09.md
Original file line number Diff line number Diff line change
@@ -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: `<Navigate to="//attacker.example" />` 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
Loading