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
80 changes: 80 additions & 0 deletions security/pentest-2026-06-15.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Pentest mini-report — versila22/lima-app — 2026-06-15

**Probed URL:** https://limaimpro.duckdns.org/
**Stack:** React 18 / Vite 7 / FastAPI + SQLAlchemy (Python) / PWA=no (icons present, no manifest/SW)
**Counts:** Critical=0 High=4 Medium=3 Low=1 Info=1

## Findings

| Sev | Cat | Title | Location |
|---|---|---|---|
| High | SAST | Default FRONTEND_URL auto-injected into CORS with allow_credentials=True | `backend/app/config.py:37,84-87` |
| High | SAST | Hardcoded admin seed password in production startup code | `backend/app/main.py:54` |
| High | SCA | vitest <3.2.6 — arbitrary file read/exec via UI server (GHSA-5xrq-8626-4rwp) | `package.json devDeps` |
| High | SCA | serialize-javascript ≤7.0.2 — RCE via malicious RegExp (GHSA-5c6j-r48x-rmvq) | `package.json devDeps` |
| Medium | DAST | Unauthenticated /health/db + /health/migrations expose DB URL prefix and full stack trace | `backend/app/main.py:202,224` |
| Medium | SCA | 6 moderate npm vulns: postcss, @rollup/plugin-terser, brace-expansion, react-router, react-router-dom, workbox-build | `package.json` |
| Medium | SCA | 3 further high npm devDep vulns: @babel/plugin-transform-modules-systemjs, esbuild, fast-uri | `package.json` |
| Low | Infra | nginx CSP: style-src includes 'unsafe-inline' | `nginx.conf:10` |
| Info | Infra | TLS 1.0/1.1 disabled; TLS 1.2+1.3 confirmed enabled | `limaimpro.duckdns.org:443` |

## Top 3 fixes
1. **CORS default FRONTEND_URL** — Set `FRONTEND_URL` explicitly in Railway production env vars (e.g. `https://limaimpro.duckdns.org`) so the `improv-cabaret-planner.lovable.app` default is never injected.
2. **Seed admin password** — Replace hardcoded `"Admin1234!"` with a startup check: raise an error if `SEED_ADMIN_PASSWORD` env var is unset; never commit passwords to source.
3. **Health endpoint auth** — Add `Depends(require_admin)` to `/health/db` and `/health/migrations`, or move stack-trace output to logs only (remove from HTTP response).

## Evidence (High findings)

### CORS default FRONTEND_URL auto-injection — `backend/app/config.py:37,84-87`
```python
FRONTEND_URL: str = "https://improv-cabaret-planner.lovable.app" # default if env unset
# …
def inject_frontend_url_into_cors(self) -> "Settings":
if self.FRONTEND_URL and self.FRONTEND_URL not in self.CORS_ORIGINS:
self.CORS_ORIGINS = list(self.CORS_ORIGINS) + [self.FRONTEND_URL]
```
**Impact:** If FRONTEND_URL is not set in Railway production, a potentially-uncontrolled Lovable.app subdomain is granted credentialed CORS access (`allow_credentials=True`). An attacker who acquires `improv-cabaret-planner.lovable.app` can exfiltrate user sessions from victim browsers.
**Fix:** Explicitly set `FRONTEND_URL=https://limaimpro.duckdns.org` in all production environments; add a startup assertion to fail loudly if it still equals the lovable.app default.

### Hardcoded admin seed password — `backend/app/main.py:54`
```python
{"email": "admin@lima-impro.fr", "app_role": "admin", "password": "Admin1234!", …}
```
**Impact:** On a fresh DB deploy, admin account is created with a well-known password visible in the git history. If the password is not changed post-deploy, full admin access is exposed.
**Fix:** Remove password from source; seed from `SEED_ADMIN_PASSWORD` env var or generate a random password printed once to startup logs.

### vitest <3.2.6 — GHSA-5xrq-8626-4rwp
```
"vitest": "^3.2.4" → resolves to 3.2.4 (patched: ≥3.2.6)
```
**Impact:** If Vitest UI server (`--ui`) runs on a CI host reachable from the network, arbitrary files can be read and executed. Affects CI pipelines with `--ui` flag.
**Fix:** Upgrade vitest to ≥3.2.6 (`npm install vitest@latest`).

### serialize-javascript ≤7.0.2 — GHSA-5c6j-r48x-rmvq
```
serialize-javascript: ≤7.0.2 (vulnerable to RCE via crafted RegExp)
```
**Impact:** Build-time code path; if serialized user-controlled input reaches this, RCE during build.
**Fix:** Upgrade affected transitive deps or pin `serialize-javascript` ≥8.0.0.

## Verified safe
- No XSS sinks (innerHTML / dangerouslySetInnerHTML / eval) in frontend source
- No secrets committed to source (VITE_* env files contain only public API URL)
- JWT: non-default secret enforced in non-dev mode by validator (config.py:63-70)
- Cookies: httpOnly=True + Secure=True on HTTPS + SameSite=none (security.py:119-131)
- CORS: explicit allowlist, no wildcard `*`; allow_credentials=True pairs correctly with explicit origins
- Admin routes protected by `require_admin` dependency
- Rate limiting: slowapi applied globally
- Security headers middleware: HSTS 1y, nosniff, X-Frame-Options: DENY, CORP, COOP, strict CSP on API
- nginx: HSTS, nosniff, DENY framing, Referrer-Policy, Permissions-Policy all set
- TLS 1.0 / 1.1 disabled; TLS 1.2 + 1.3 enabled
- No .env / .git files exposed (403 on all probed paths)

## Needs server-side verification
- Actual FRONTEND_URL value in Railway production environment
- Whether CORS `allow_credentials` + origin list is correct in live API responses (DAST blocked from this network)
- Response headers from live API (HSTS, CSP, nosniff) — middleware tested only via source review
- Admin seed account: confirm `admin@lima-impro.fr` password was changed post first-deploy

## Tools
ran=npm-audit, grep-secret-scan, openssl-tls-check; skipped=DAST-curl (Anthropic egress gateway blocks target hosts — HTTP 403 host_not_allowed), pnpm/yarn (not present)
Loading