Reproduction steps:
- curl -I https://railcall.ai/ -> no Strict-Transport-Security, no Content-Security-Policy
- curl -I https://railcall.ai/dashboard/ -> same (this origin serves the login + dashboard SPA)
- For contrast, the marketplace API DOES send both:
curl -I https://railcall-marketplace-lggm.onrender.com/ -> HSTS + CSP present
- In the served client JS, auth material is kept in localStorage:
marketplace_access_token, marketplace_refresh_token, railcall_key, railcall_session
Expected:
The origin that serves the authenticated app should set HSTS (and ideally a CSP),
at least as strong as the API already does — especially when it also hands the
browser long-lived credentials.
Actual:
railcall.ai (and /dashboard) return neither Strict-Transport-Security nor
Content-Security-Policy. So the app-serving origin has:
- no HSTS: a first visit (or an active stripped-TLS MITM) can be served over
HTTP and the login/session intercepted before any upgrade;
- no CSP on this origin: an injected script has no policy backstop, and since a
long-lived refresh token lives in localStorage, any XSS reads it directly for
account takeover.
Severity is low on its own — this is defense-in-depth, not an exploited bug —
but the combination (no HSTS + no CSP + a refresh token in localStorage on the
credentialed origin) is worth closing for a security-first product.
Suggested fix:
- Add
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
on railcall.ai, matching the marketplace API.
- Serve a Content-Security-Policy on the app origin.
- Consider moving the refresh token out of localStorage into an httpOnly,
Secure, SameSite=Strict cookie, so an XSS can't read it.
Found via passive header inspection (curl -I) — no probing, nothing exploited.