Skip to content

SPA Security β€” Protecting Single-Page Applications from Vulnerabilities

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about SPA Security. We cover key concepts, practical examples, and best practices to help you master this topic.

SPA security covers XSS prevention, CSRF protection, Content Security Policy headers, secure token storage, dependency auditing, and OWASP top 10 mitigation for client-side applications.

What You'll Learn

By the end of this tutorial, you will understand the main security threats facing SPAs, how to prevent XSS and CSRF Attacks, implement Content Security Policy, store tokens securely, audit dependencies for vulnerabilities, and follow OWASP guidelines for single-page applications.

Why It Matters

SPAs are more vulnerable to client-side attacks than traditional MPAs because more logic runs in the browser. A single XSS vulnerability can expose user tokens, personal data, and allow attackers to perform actions as the victim. Security is not optional β€” it protects your users and your reputation.

Real-World Use

A financial SPA suffered a stored XSS attack when user profile names were rendered without sanitization. The attack stole session tokens from 5,000 users and performed unauthorized transactions. After implementing CSP, input sanitization, and HttpOnly cookies, no further attacks succeeded.

SPA Attack Surface
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚                    SPA Attack Vectors                     β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    β”‚             β”‚              β”‚             β”‚               β”‚
    β”‚  XSS        β”‚  CSRF        β”‚  Token      β”‚  Dependency  β”‚
    β”‚  (Stored,   β”‚  (Cross-Site β”‚  Theft      β”‚  Vulnerab-   β”‚
    β”‚  Reflected, β”‚  Request     β”‚  (LocalStor β”‚  ilities     β”‚
    β”‚  DOM-based) β”‚  Forgery)    β”‚  age access)β”‚  (npm audit) β”‚
    β”‚             β”‚              β”‚             β”‚               β”‚
    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚             β”‚              β”‚              β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”
    β”‚ Sanitize    β”‚ β”‚ Anti-    β”‚ β”‚ HttpOnly  β”‚ β”‚ Regular    β”‚
    β”‚ input       β”‚ β”‚ CSRF     β”‚ β”‚ Cookies   β”‚ β”‚ Dependency β”‚
    β”‚ DOMPurify   β”‚ β”‚ tokens   β”‚ β”‚ (not LS)  β”‚ β”‚ auditing   β”‚
    β”‚ CSP headers β”‚ β”‚ SameSite β”‚ β”‚ Secure    β”‚ β”‚ Snyk/Dependβ”‚
    β”‚             β”‚ β”‚ cookies  β”‚ β”‚ flag      β”‚ β”‚ abot       β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Think of SPA security like securing a house where the front door is always unlocked (client-side code). You cannot stop people from seeing the door, but you can put valuables in a safe (HttpOnly cookies), install cameras (CSP), train the family not to let strangers in (input sanitization), and regularly check for weak spots (dependency audits).

XSS Prevention with DOMPurify

import DOMPurify from 'dompurify';

// User-generated content must be sanitized before rendering
function CommentSection({ comments }) {
    return (
        <div>
            {comments.map(comment => (
                <div key={comment.id}>
                    {/* NEVER use dangerouslySetInnerHTML without sanitization */}
                    <div
                        dangerouslySetInnerHTML={{
                            __html: DOMPurify.sanitize(comment.body, {
                                ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
                                ALLOWED_ATTR: ['href', 'title'],
                                ALLOW_DATA_ATTR: false
                            })
                        }}
                    />
                    <small>
                        β€” {DOMPurify.sanitize(comment.author)}
                    </small>
                </div>
            ))}
        </div>
    );
}

// Also sanitize URL inputs
function safeUrl(url) {
    const allowedProtocols = ['https:', 'http:', 'mailto:'];
    try {
        const parsed = new URL(url);
        if (!allowedProtocols.includes(parsed.protocol)) {
            return '';
        }
        return url;
    } catch {
        return '';
    }
}

// Expected output for input '<script>alert("xss")</script>':
//   DOMPurify removes the script tag entirely
//   Output: ''
//
// Expected output for '<b>Hello</b>':
//   Output: '<b>Hello</b>'

CSRF Protection in SPAs

// 1. SameSite cookie attribute (modern approach)
// Server-side: Set-Cookie header
// Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly

// 2. Anti-CSRF token in custom header
const apiClient = {
    baseUrl: '/api',
    csrfToken: null,

    async init() {
        // Fetch CSRF token on app load
        const response = await fetch(`${this.baseUrl}/csrf-token`, {
            credentials: 'include'
        });
        const data = await response.json();
        this.csrfToken = data.token;
    },

    async request(endpoint, options = {}) {
        const headers = {
            'Content-Type': 'application/json',
            'X-CSRF-Token': this.csrfToken,
            ...options.headers
        };

        const response = await fetch(`${this.baseUrl}${endpoint}`, {
            ...options,
            headers,
            credentials: 'include'
        });

        if (response.status === 403) {
            // CSRF token invalid β€” re-fetch and retry
            await this.init();
            return this.request(endpoint, options);
        }

        return response;
    }
};

// 3. Double-submit cookie pattern
function doubleSubmitCsrf() {
    const cookieCsrf = document.cookie
        .split('; ')
        .find(row => row.startsWith('csrf-token='))
        ?.split('=')[1];

    const headerCsrf = document.querySelector('meta[name="csrf-token"]')
        ?.getAttribute('content');

    // Both must match
    return cookieCsrf === headerCsrf;
}

Content Security Policy

// CSP header (set on server)
// Content-Security-Policy:
//   default-src 'self';
//   script-src 'self' https://cdn.example.com;
//   style-src 'self' 'unsafe-inline';
//   img-src 'self' https: data:;
//   connect-src 'self' https://api.example.com;
//   frame-ancestors 'none';
//   form-action 'self';

// Report CSP violations from the client
document.addEventListener('securitypolicyviolation', (event) => {
    const report = {
        violated: event.violatedDirective,
        blocked: event.blockedURI,
        page: window.location.href,
        userAgent: navigator.userAgent
    };

    fetch('/api/csp-report', {
        method: 'POST',
        body: JSON.stringify(report),
        headers: { 'Content-Type': 'application/json' }
    });
});

// Sample CSP violation report:
// {
//   "violatedDirective": "script-src-elem",
//   "blockedURI": "https://evil.com/malicious.js",
//   "page": "https://example.com/dashboard",
//   "userAgent": "Mozilla/5.0 ..."
// }

Common Mistakes

  1. Storing tokens in localStorage or sessionStorage. Any XSS vulnerability can read localStorage. Use HttpOnly cookies that are inaccessible to JavaScript for sensitive tokens.
  2. Trusting client-side input validation. Client-side validation is for UX, not security. Always validate and sanitize on the server too. Attackers can bypass client-side checks easily.
  3. No Content Security Policy. Without CSP, any injected script executes freely. CSP provides a defense-in-depth layer that catches XSS even if input sanitization fails.
  4. Using dangerouslySetInnerHTML without sanitization. React's JSX escapes values by default, but dangerouslySetInnerHTML bypasses all protection. Always sanitize with DOMPurify.
  5. Not auditing dependencies. SPAs depend on hundreds of npm packages. A single vulnerable dependency can compromise your entire application. Run npm audit regularly.

Practice Questions

  1. Why is storing JWT tokens in localStorage dangerous for SPAs?
  2. How does a Content Security Policy protect against XSS Attacks?
  3. What is the difference between SameSite=Strict and SameSite=Lax cookies?
  4. How does DOMPurify prevent XSS when using dangerouslySetInnerHTML?
  5. What is the double-submit cookie pattern for CSRF protection?

Challenge: Perform a security audit on an existing SPA. Check for: token storage method (localStorage vs HttpOnly cookies), CSP headers, input sanitization on user-generated content, SameSite cookie attributes, and dependency vulnerabilities with npm audit. Fix all findings and verify with OWASP ZAP scanner.

FAQ

Can SPAs be completely secure?

No application is completely secure. But you can reduce risk by following security best practices: CSP headers, HttpOnly cookies, input sanitization, regular dependency audits, and server-side validation.

Is React immune to XSS?

No. React escapes values in JSX by default, but dangerouslySetInnerHTML, href attributes, and server-side rendering can introduce XSS if not handled carefully.

Should I use localStorage or sessionStorage for tokens?

Neither. Use HttpOnly, Secure, SameSite cookies for tokens. If you must use client-side storage, sessionStorage is slightly safer than localStorage (cleared on tab close) but still vulnerable to XSS.

What is the most common SPA security vulnerability?

XSS (Cross-Site Scripting) is the most common. It occurs when user input is rendered without sanitization, allowing attackers to inject malicious scripts.

How often should I audit dependencies?

Run npm audit or yarn audit on every build. Use Dependabot or Snyk for automated vulnerability monitoring. Review and update dependencies at least monthly.

Mini Project

Build a user profile SPA with the following security measures: HttpOnly cookies for authentication tokens, Content Security Policy headers that restrict script sources, DOMPurify sanitization for user bios and comments, SameSite=Strict cookies, and automated npm audit in the CI pipeline. Verify with OWASP ZAP scanner.

What's Next

You understand SPA security. Now explore SPA authentication to implement secure login, JWT management, and OAuth flows in your application.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro