Testing Single Page Applications for Security Issues
Single-page applications shifted a meaningful amount of application logic from the server to the browser, and testing methodology built around traditional server-rendered, multi-page applications doesn't fully transfer. A crawler designed to follow <a href> links and submit <form> elements will barely scratch the surface of a React or Vue application where routing, rendering, and a good portion of the "page" itself is generated by JavaScript after the initial load. SPA security testing needs its own checklist.
The crawling problem, and why it matters
Traditional dynamic scanners rely on discovering the application's surface by crawling links and forms in server-rendered HTML. An SPA's initial HTML payload is often close to empty — a single root <div> with the real content and navigation generated client-side after JavaScript execution and, frequently, after one or more API calls. A scanner that doesn't execute JavaScript and wait for the app to render will see almost none of the actual application. Effective SPA testing requires either a scanner configured to run a real browser engine and interact with the rendered DOM, or — often better — testing the underlying API directly, since that's genuinely where the SPA's server-side logic and data access live. The frontend is largely a rendering layer; the security-relevant enforcement needs to happen in the API regardless of how the SPA's UI behaves.
Client-side routing is not access control
SPAs typically implement client-side route guards — a router configuration that redirects an unauthenticated or under-privileged user away from a protected route before rendering it. This is a legitimate and useful UX feature, but it is not a security control, and it's a mistake seen often enough to call out specifically: a route guard prevents the component from rendering in that browser tab, but it does nothing to stop a user from calling the underlying API endpoints directly with a tool like curl or Postman. Every protected route's data must be independently protected by server-side authorization; the router guard is a convenience layer on top of that, never a substitute for it.
Where sensitive data ends up in SPAs specifically
Token storage. Where the SPA stores its access token has direct security consequences. localStorage and sessionStorage are readable by any JavaScript running on the page, which means a single successful XSS vulnerability anywhere in the application can exfiltrate the token. HttpOnly cookies are not readable by JavaScript at all, which is meaningfully more resistant to XSS-based token theft, at the cost of needing CSRF protections instead. This tradeoff should be a deliberate architectural decision, not a default nobody revisited.
Bundled secrets. Build tooling occasionally bakes API keys, internal endpoint URLs, or feature-flag configuration directly into the JavaScript bundle shipped to the browser. Anything in the client bundle is fully readable by inspecting network traffic or the built files directly — it should never include anything that would matter if an attacker read it, including third-party API keys with meaningful scopes.
Source maps in production. Source maps make debugging production issues dramatically easier, but if deployed publicly they also hand an attacker a readable, de-minified view of your entire application logic, including comments, variable names, and sometimes hardcoded values that were never meant to be exposed. If source maps are needed for error-tracking tooling, they should be uploaded directly to that tooling rather than served publicly alongside the app.
postMessage and cross-origin communication. SPAs that embed iframes or communicate with other windows via postMessage need to validate the origin of incoming messages explicitly; a handler that accepts messages from any origin is a direct injection point, particularly relevant for DOM-based XSS.
A practical SPA testing checklist
- Test the API directly and independently of the frontend, since that's where authorization and data validation actually need to be enforced.
- Confirm every client-side route guard has a matching server-side authorization check on the corresponding API calls.
- Review token storage strategy (localStorage vs. HttpOnly cookies) as a deliberate tradeoff against the application's actual XSS exposure and CSRF mitigations.
- Inspect the built JavaScript bundle for hardcoded secrets, internal URLs, or overly permissive API keys before every release.
- Confirm production builds don't serve public source maps, or that source maps are routed only to authenticated error-tracking tooling.
- Validate origin checks on any
postMessagehandlers and cross-window communication.
Because SPA testing genuinely spans both the client-side bundle and the backing API, and because that split doesn't map cleanly onto a traditional single-target scan, it's an area where scope definition matters more than usual. Venstap's asset model lets a web application and its underlying API be scoped and scanned as related but distinct assets, so findings from the frontend bundle review and the API-level testing land in the same triage queue without one side accidentally being assumed to be someone else's responsibility.
Ready to see Venstap in action?
Get a guided walkthrough of scanning, triage, and reporting on your own assets.