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
91 changes: 91 additions & 0 deletions security/pentest-2026-06-10.md
Original file line number Diff line number Diff line change
@@ -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
Loading