SPA Security β Protecting Single-Page Applications from Vulnerabilities
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
- Storing tokens in localStorage or sessionStorage. Any XSS vulnerability can read localStorage. Use HttpOnly cookies that are inaccessible to JavaScript for sensitive tokens.
- 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.
- 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.
- Using dangerouslySetInnerHTML without sanitization. React's JSX escapes values by default, but dangerouslySetInnerHTML bypasses all protection. Always sanitize with DOMPurify.
- 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
- Why is storing JWT tokens in localStorage dangerous for SPAs?
- How does a Content Security Policy protect against XSS Attacks?
- What is the difference between SameSite=Strict and SameSite=Lax cookies?
- How does DOMPurify prevent XSS when using dangerouslySetInnerHTML?
- 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
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