Skip to content

Security audit β€” 2026-06-30#79

Open
versila22 wants to merge 1 commit into
mainfrom
security-audit-2026-06-30
Open

Security audit β€” 2026-06-30#79
versila22 wants to merge 1 commit into
mainfrom
security-audit-2026-06-30

Conversation

@versila22

Copy link
Copy Markdown
Owner

Security audit β€” 2026-06-30

Probed URL: https://limaimpro.duckdns.org/
Stack: React 18 / Vite 7 / shadcn-ui / FastAPI (Python) / PostgreSQL / npm / PWA=yes
Counts: Critical=1 High=4 Medium=3 Low=2 Info=2


Findings

Sev Cat Title Location
Critical SAST Vitest UI β€” arbitrary file read/execute package.json vitest@3.2.4 (GHSA-5xrq-8626-4rwp)
High SAST User enumeration via distinct login error messages backend/app/routers/auth.py:64-70
High SAST Rate-limit bypass via X-Forwarded-For spoofing backend/app/limiting.py:14-16
High SAST Base64 photo stored in DB with no size cap backend/app/routers/members.py (/photo-data endpoint)
High SAST react-router open redirect (//path) package.json react-router@6.30.3 (GHSA-2j2x-hqr9-3h42)
Medium SAST CSP style-src 'unsafe-inline' weakens XSS protection nginx.conf:14
Medium SAST python-jose 3.5.0 β€” outdated JWT library (unmaintained) backend/requirements.txt
Medium Infra serialize-javascript RCE in build toolchain package.json serialize-javascript@6.0.2 (devDep)
Low SAST HSTS missing preload directive nginx.conf:15
Low Infra TLS 1.2 still enabled (TLS 1.3-only preferred) DAST β€” openssl probe
Info SAST Login returns access token in body (dual-channel) backend/app/routers/auth.py
Info Infra TLS cert valid 30 days only (Let's Encrypt, auto-renews via Traefik) DAST

Top 3 fixes

  1. User enumeration β€” Collapse all 3 auth error branches into a single generic message ("Identifiants invalides") and delete the _AUTH_MESSAGES dict in backend/app/routers/auth.py.
  2. X-Forwarded-For rate-limit bypass β€” Replace forwarded_for.split(",")[0].strip() with the rightmost untrusted IP or use a trusted-proxy list; alternatively use slowapi's get_remote_address key function which reads request.client.host.
  3. Base64 photo size cap β€” Add a size guard (if len(payload.data) > 500_000: raise HTTPException(413)) in upload_member_photo_data before the DB write.

Evidence (Critical/High only)

[CRITICAL] Vitest UI β€” arbitrary file read/execute

  • Location: package.json devDependency vitest@3.2.4 (GHSA-5xrq-8626-4rwp, fixed in 3.2.6)
  • Snippet: "vitest": "^3.2.4" β€” installed version is 3.2.4
  • Impact: If vitest --ui is inadvertently exposed (e.g., dev server port open) any file on the host can be read or executed by a remote attacker.
  • Fix: npm update vitest to >=3.2.6; confirm vitest UI port is never reachable from the internet in CI/CD.

[HIGH] User enumeration via distinct login error messages

  • Location: backend/app/routers/auth.py lines 62-72
  • Snippet: "email_not_found": "Aucun compte trouvΓ© pour cet email.", "wrong_password": "Mot de passe incorrect.", ...
  • Impact: Attacker can enumerate which email addresses have registered accounts, facilitating targeted phishing or credential stuffing.
  • Fix: Return a single message for all failure cases: "Identifiants invalides.".

[HIGH] Rate-limit bypass via X-Forwarded-For spoofing

  • Location: backend/app/limiting.py lines 14-16
  • Snippet: forwarded_for = request.headers.get("X-Forwarded-For"); return forwarded_for.split(",")[0].strip()
  • Impact: Attacker sets a spoofed X-Forwarded-For header to rotate apparent source IP, bypassing the 5/min login rate limit and enabling unlimited brute-force.
  • Fix: Use the rightmost (trusted) IP from X-Forwarded-For, or switch key function to get_remote_address and configure Railway to strip/inject the header reliably.

[HIGH] Unbounded base64 photo stored in DB

  • Location: backend/app/routers/members.py β€” upload_member_photo_data endpoint (POST /members/{id}/photo-data)
  • Snippet: member.photo_url = payload.data with no size check after startswith("data:image/")
  • Impact: Any authenticated member can upload arbitrarily large base64 strings into the photo_url column, causing DB bloat, OOM on the API process, and potential DoS.
  • Fix: Enforce a max length (e.g., 500 KB) on payload.data before persisting; consider moving all photos to R2 exclusively.

[HIGH] react-router open redirect

  • Location: package.json β€” react-router-dom@6.30.3 (GHSA-2j2x-hqr9-3h42, fixed in 6.30.4)
  • Snippet: react-router-dom: ^6.30.1 β€” installed 6.30.3; paths starting with // are reinterpreted as protocol-relative URLs.
  • Impact: Attacker crafts a link like https://app/#//evil.com/path; react-router redirects user to //evil.com, enabling phishing.
  • Fix: npm update react-router-dom to >=6.30.4.

Verified safe

  • No hardcoded secrets in .ts/.tsx/.js/.jsx source β€” all keys use VITE_ prefix from env vars.
  • No dangerouslySetInnerHTML, innerHTML =, or eval() found in React frontend source.
  • No postMessage usage that could allow cross-origin message injection.
  • JWT tokens use httpOnly cookies; not stored in localStorage.
  • API docs (/docs, /redoc) disabled in production.
  • Admin endpoints correctly guarded by require_admin dependency.
  • TLS 1.0 and TLS 1.1 disabled; TLS 1.2 + 1.3 enabled.
  • TLS cert valid (notBefore: 2026-06-30, notAfter: 2026-07-30); auto-renew via Traefik.
  • Sensitive paths (.env, .git/config, backup.zip) non-200.
  • python-jose JWT decode uses explicit algorithms= β€” alg:none mitigated.
  • HSTS present, X-Frame-Options: DENY, X-Content-Type-Options: nosniff set.
  • CORS allow_origins is an explicit list, not wildcard.

Needs server-side verification

  • Confirm Railway strips/sanitises X-Forwarded-For before reaching the API.
  • Verify APP_ENV=production on Railway so validate_jwt_secret rejects default JWT secret at startup.
  • Verify REFRESH_JWT_SECRET is distinct from JWT_SECRET in Railway env vars.
  • Confirm Vitest UI port is not exposed externally in CI/CD.

Tools

ran=npm-audit, openssl-s_client (TLS), curl (HTTP headers/sensitive-path probe), grep/find (SAST), manual code review
skipped=pnpm-audit (not installed), yarn-audit (not installed), safety-check (network-blocked), nmap/nikto (not installed)


πŸ€– Generated with Claude Code

https://claude.ai/code/session_018PAhc6W5WwV24qacqWB6ao


Generated by Claude Code

…-06-09)

Critical=1 High=4 Medium=3 Low=2 Info=2
New findings: vitest RCE (GHSA-5xrq-8626-4rwp), user enumeration,
X-Forwarded-For rate-limit bypass, unbounded base64 photo upload,
react-router open redirect (GHSA-2j2x-hqr9-3h42).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PAhc6W5WwV24qacqWB6ao
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant