Security threats in SPAs and how to defend against them
A developer’s guide to identifying and fixing the most common security flaws in Single-page applications.
Single-page applications (SPAs) dominate the modern web for good reason: they offer fast navigation, rich interactivity, and efficient API communication. However, this shift to the client has also made security responsibilities more dangerous.
In traditional web apps, every page load passes through the server, where permissions and sessions are verified in a controlled environment. But in SPAs, the browser becomes the primary runtime environment, and that changes everything.
Instead of relying on the server to validate each action or protect resources, SPAs often hold tokens in browser storage, decide which UI elements to show based on locally stored roles, and manage route access with JavaScript. This means that security decisions—like “is this user allowed to see this page?” or “can they make this API call?”—are now happening in the most exposed place possible: the browser, where attackers can inspect, modify, or replay requests with little effort.
In this article, we dive deep into SPA-specific vulnerabilities and show how to implement practical defenses for enterprise-grade security.
Threat 1: Cross-Site Scripting (XSS)
XSS is one of the most widespread and dangerous web security vulnerabilities. It occurs when an attacker manages to inject malicious JavaScript into your application—code that then executes in the browser of another user as if it came from a trusted source.
SPAs are especially prone to XSS because they often render content dynamically, updating the page using JavaScript rather than server-rendered HTML. If your app inserts user-generated data into the DOM without proper sanitization or escaping, it creates an opening for attackers to inject harmful scripts.
Imagine you render user-submitted content like this:
If comment
comes from a user, they could inject a script that runs in someone else's browser.
To avoid this threat:
- Avoid
innerHTML
. Use libraries like React or Vue, which escape user content by default:
- If you must render raw HTML, sanitize it first using a library like DOMPurify:
- Add a Content Security Policy (CSP) to block inline scripts:
Threat 2: Token storage and theft
Most SPAs authenticate users using access tokens (like JWTs) from an identity provider or backend service. These tokens prove who the user is when making API calls.
Many developers store these tokens in localStorage
or sessionStorage
because it’s easy. But that means any XSS attack can read them.
Example (bad practice):
Tokens should only be stored in cookies that are:
HttpOnly
: not accessible by JavaScriptSecure
: only sent over HTTPSSameSite=Strict
: not sent on cross-site requests
Example server response:
Even if your app has an XSS vulnerability, the attacker can’t read HttpOnly cookies—but your browser will still send them with API requests.
Threat 3: Broken Object-Level Authorization (BOLA)
Broken Object-Level Authorization (BOLA)—also known as Insecure Direct Object Reference (IDOR)—occurs when your backend exposes resources (like user profiles, files, orders) using identifiers (like user_id
, file_id
, etc.), but fails to check whether the requesting user is actually allowed to access that object.
It’s one of the top vulnerabilities listed by the OWASP API Security Top 10 because it’s common, easy to exploit, and often overlooked.
In a Single-page application:
- The frontend talks directly to backend APIs.
- All the API endpoints are visible in the browser (via DevTools or a proxy like Burp Suite).
- Any authenticated user can tamper with requests, including changing IDs in URLs, query strings, or request bodies.
If your backend doesn't explicitly enforce ownership or access control, users can view or manipulate resources they shouldn't.
Example:
If a logged-in user changes 1234
to 1235
, do they see another user's orders?
To avoid this issue, the backend should never trust the user implicitly. Always check the authenticated user's identity on the server.
Threat 4: Insecure routing and fake protection
SPAs manage routing in the browser, so users can visit any “route” in the app. Some developers rely only on hiding UI elements to restrict access.
This is a problem because anyone can open DevTools, change localStorage, or type in a route manually. Frontend protections are not enough.
Example (fake protection):
A malicious user could simply set role=admin
in localStorage.
What you should do instead, is protect data access on the server, not in the UI:
Client-side route guards are helpful for UX, but never assume they’re secure.
Most SPAs validate user input—like emails, passwords, or form fields—directly in the browser. This is great for user experience, but it's not a real security boundary.
Attackers aren’t limited to your UI. They can bypass the frontend entirely and send requests directly to your backend using tools like Postman, curl, or custom scripts. That means if your server blindly trusts incoming data, it may receive malformed, malicious, or even dangerous input—regardless of how carefully you designed the frontend.
To fix this, you need to validate all input on the server. One of the most effective ways to do this is by using a schema validation library like Zod (for TypeScript) or Joi (for JavaScript/Node.js). These libraries let you define exactly what a valid request should look like—and reject anything that doesn’t match.
Example using Zod:
You can even share the schema with your frontend using libraries like zod
+ ts-rest
.
By enforcing validation at the edge of your API, you protect your app from malformed data, injection attacks, and logic errors—no matter how the request was made.
Always treat frontend validation as a convenience, not a defense mechanism.
Threat 5: Session and token expiry problems
SPAs often use JWTs that live a long time. If the user logs out or gets deactivated, they can still access your APIs unless you handle it explicitly.
A safer strategy is to use refresh token rotation:
- Use short-lived access tokens (e.g., 15 minutes).
- Store a refresh token securely (e.g.,
HttpOnly
cookie). - On refresh:
- Revoke the old refresh token.
- Issue a new pair of tokens.
- Track token reuse—reused refresh tokens = possible theft.
Bonus tips for hardening SPA security
Beyond the major threats, there are several additional best practices that can make your SPA significantly more resilient to common attacks and abuse. These are easy to overlook, but they help close important security gaps:
- Use Subresource Integrity (SRI) when loading external scripts: If your SPA relies on scripts from third-party CDNs (like analytics or UI libraries), SRI lets the browser verify that the file hasn't been tampered with. By including a cryptographic hash, you ensure the browser only runs scripts that match the expected content—even if the CDN is compromised.
- Keep third-party dependencies up to date (watch for CVEs): Your frontend and backend code likely rely on dozens (or hundreds) of npm packages. Many security vulnerabilities—especially XSS and prototype pollution—are introduced through outdated dependencies. Use tools like
npm audit
, Snyk, or Dependabot to monitor and patch known CVEs (Common Vulnerabilities and Exposures) regularly. - Enable CSP, X-Frame-Options, and X-Content-Type-Options headers: These HTTP headers harden your app against a variety of attacks:
Content-Security-Policy (CSP)
prevents inline scripts and controls which domains can serve scripts, styles, or images.X-Frame-Options: DENY
blocks your app from being embedded in an<iframe>
, preventing clickjacking attacks.X-Content-Type-Options: nosniff
tells browsers not to guess file types—helping stop MIME-type confusion attacks.
- Log suspicious behavior and use rate limiting to detect abuse: Monitor patterns like excessive failed logins, rapid-fire API requests, or repeated access to sensitive endpoints. Logging this activity gives you visibility into potential abuse or attempted exploitation. Combine it with rate limiting (using middleware like
express-rate-limit
or services like Cloudflare) to throttle attackers and protect your infrastructure from brute-force attacks or credential stuffing. For a more sophisticated tool that can do all of these and more, check out WorkOS Radar.
How WorkOS can help
- Hosted login flows with proper cookie-based sessions
- Secure token storage and rotation
- SAML, OIDC, and Magic Link auth with minimal setup
By outsourcing the hard parts of auth, you reduce your exposure to many of these risks.
WorkOS makes it easy to integrate secure, enterprise-grade identity into SPAs without reinventing authentication or user management. It directly addresses many of the security risks SPAs face:
- SAML & OIDC support for 25+ identity providers (Okta, Azure AD, Google Workspace, OneLogin, Ping, ADFS, and more) with fully managed hosted login flows that keep tokens out of frontend storage.
- Built-in SCIM 2.0 provisioning enables automatic user creation, updates, and deactivation—critical for keeping access lists current and preventing unauthorized reuse of stale sessions.
- RBAC and FGA support lets you model role-based and attribute-based access directly from token claims and organization context—critical for securely managing user permissions and object-level ownership.
- Admin Portal gives IT teams a self-serve interface to configure and test SSO and SCIM—reducing support load and ensuring secure setup without custom tooling.
- Vault securely stores sensitive credentials (e.g., SAML certificates, SCIM tokens) with lifecycle management, eliminating the risk of exposing secrets in frontend code or config files.
- Radar provides real-time visibility into authentication events, provisioning activity, and identity flows so you can audit access, detect, and block suspicious behavior.
WorkOS helps shift critical identity and access logic out of the browser and into secure, centralized systems—making your SPA more secure by default and enterprise-ready.
Final thoughts
Single Page Applications offer a fast, flexible user experience—but that flexibility comes with security trade-offs. When core logic moves to the client, risks like XSS, token leakage, and broken access control become much harder to manage without a thoughtful architecture.
The key to securing SPAs is shifting critical trust boundaries back to the server: validate input, enforce object-level access control, store tokens securely, and never assume the frontend is a safe place for sensitive logic.
By layering in strong identity, lifecycle management, and backend enforcement—with tools like WorkOS—you can build SPAs that not only scale, but also meet the security and compliance demands of enterprise customers.