Frontend security has a perception problem: people think it's the backend's job. The backend does carry the heavier responsibility — auth, authorization, data integrity. But there's a real category of attacks that live entirely in the browser, and the frontend is the only place to defend against them.

This is the basics. Not "advanced exploit chains" — just the four categories of bugs you should be able to recognize and prevent.

XSS Starts With Unsafe Rendering

Cross-site scripting is when attacker-controlled content gets executed as code in the user's browser. The classic example: a comment field that lets users type <script>steal()</script>, the page renders it as HTML, the script runs in every visitor's browser.

The fix is rendering user content as text, not as HTML:

JSX
// ✅ React escapes by default — safe
function Comment({ text }: { text: string }) {
  return <p>{text}</p>;
}

// 🐛 dangerouslySetInnerHTML bypasses the escape
function CommentRaw({ text }: { text: string }) {
  return <p dangerouslySetInnerHTML={{ __html: text }} />;
}

Modern frameworks escape by default. dangerouslySetInnerHTML (React), v-html (Vue), {@html} (Svelte) all opt out of that protection. Use them only with content you control or content you've sanitized with a library like DOMPurify.

The other layer is a Content Security Policy header from the server, telling browsers which scripts are allowed to run:

Http
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com; object-src 'none';

A strict CSP turns "the attacker injected a script" into "the browser refused to run it." Set one early; tightening later is harder than starting strict.

CSRF Depends On Browser Behavior

Cross-site request forgery exploits a feature: cookies are sent automatically to their origin. If you're logged into your bank, and a malicious site loads <img src="https://bank.com/transfer?to=attacker&amount=1000">, your browser sends your bank's cookies along with the request.

Three defenses, in increasing order of strength:

  1. SameSite cookies. Setting SameSite=Lax or SameSite=Strict on session cookies prevents most cross-site requests from including them. This is the modern default in most browsers.
  2. CSRF tokens. A random token in a hidden form field that the server checks; the attacker can't read it.
  3. Origin header check. Server rejects state-changing requests where Origin doesn't match.

For SPAs that use bearer tokens in Authorization headers (not cookies), CSRF mostly disappears — the browser doesn't auto-attach Authorization. But cookie-based auth is still common, and SameSite=Lax should be the absolute minimum.

localStorage Is Convenient But Risky

The temptation: store auth tokens in localStorage because it's easy. The problem: any JavaScript on your page can read localStorage — including malicious scripts injected via XSS or supply-chain attacks. A single XSS vulnerability becomes a token leak.

The safer pattern in 2026:

  1. Auth tokens in httpOnly, Secure, SameSite cookies — set by the server, invisible to JavaScript, automatically sent with requests.
  2. Short-lived access tokens (5-15 minutes) plus a refresh token rotation flow.
  3. No long-lived secrets in the browser, ever. If it's in the JavaScript, the user can read it; assume an attacker can too.

For non-secret session state (UI preferences, cached data), localStorage is fine — but assume any attacker who runs JavaScript on your page can read it.

Annotated Content Security Policy header card. A locked-down baseline shows default-src self, script-src self plus a nonce, no inline eval, frame-ancestors none — each directive paired with a one-line note explaining what attack class it blocks.
Start with deny-by-default. Add only what you actually load.

Dependencies Are Part Of Your Attack Surface

Modern apps install hundreds of npm packages. A single malicious update to one of them — or to one of its dependencies, three levels deep — can run code in every user's browser. This isn't theoretical; it's happened multiple times to popular packages.

Defenses, in order of importance:

  1. Lockfiles. package-lock.json or pnpm-lock.yaml pins exact versions. Don't ignore the warning when one changes unexpectedly.
  2. npm audit in CI. Fails the build on known CVEs.
  3. Minimize dependencies. Every package is a trust decision. Do you need is-odd? Probably not.
  4. Pin to specific versions during high-risk releases. No automatic minor bumps for auth libraries.
  5. Subresource integrity for any externally-hosted scripts: <script integrity="sha384-...">.

Tools like Socket, Snyk, and Dependabot help, but the human habit is "be skeptical of new dependencies and old ones with sudden activity changes."

The Boring Habits Add Up

A short checklist for new projects:

Text
□ React/Vue/Svelte default escaping respected (no dangerouslySetInnerHTML)
□ CSP header set — start strict, relax only as needed
□ Auth tokens in httpOnly cookies, not localStorage
□ SameSite=Lax (minimum) on session cookies
□ npm audit in CI, dependency review on PRs
□ HTTPS everywhere (HSTS header set)
□ Subresource integrity on external scripts
□ Validate all user input on the server (don't trust frontend validation alone)
□ Don't log secrets to the console or to error trackers

None of these are clever. All of them prevent real attacks that have hit real apps in the last few years.

Pro Tips

  1. Default to escaping. Trust the framework's safe rendering; opt out deliberately.
  2. Set a strict CSP early. Loosen later if needed; tightening late is painful.
  3. Don't store auth in localStorage. httpOnly cookies survive XSS; localStorage doesn't.
  4. Audit dependencies. Once a quarter at minimum.
  5. Assume the attacker can run JavaScript. Design as if they can.

Final Tips

The frontend can't fix all security problems — auth, authorization, data validation belong to the server. But the frontend can absolutely cause security problems if it's careless about rendering, tokens, and dependencies.

The boring habits prevent the loud incidents. None of this is exciting. All of it is the difference between an application that holds together and one that becomes a postmortem.

Good luck — and may your CSP never break in production 👊