---
name: "Frontend security"
description: "Always-on browser-security rules for client-side code in any frontend stack — bundle secrets, XSS sinks, URL and redirect handling, postMessage, CSP, CSRF with cookie sessions, token storage and third-party scripts."
applyTo: "**/*.ts,**/*.tsx,**/*.js,**/*.jsx,**/*.mjs,**/*.vue,**/*.svelte,**/src/**/*.html"
---

# Frontend security rules

Everything in a browser bundle is public, and every value that reaches the DOM is attacker-influenced until proven otherwise. These are the browser-specific rules; `secure-coding.instructions.md` holds the stack-agnostic ones and is not repeated here.

## Secrets and configuration

- A browser bundle cannot hold a secret. API keys, client secrets, signing keys and connection strings belong behind your own backend; if the browser needs the capability, proxy the call.
- A key that a vendor calls "publishable" is still only safe if it is scoped and rate-limited server side. Check what it can do before treating it as public.
- Do not ship internal hostnames, private endpoints or tenant identifiers in client code, comments or source maps published to the public.

## Rendering untrusted values

- Never pass user- or API-supplied strings to an HTML sink: `dangerouslySetInnerHTML`, `[innerHTML]`, `innerHTML`, `insertAdjacentHTML`, `outerHTML`, `document.write`, `v-html`, `lwc:dom="manual"`. Render text, or render structured data as elements.
- Never call a sanitizer bypass on data you did not author: Angular's `bypassSecurityTrustHtml`/`Url`/`ResourceUrl`/`Script`/`Style` disable the framework's only XSS protection.
- If formatted user content genuinely must render, sanitise it server side with a maintained library and store the sanitised form — client-side sanitisation is a second line of defence, not the first.
- Never `eval`, `new Function`, `setTimeout('string')`, or inject a `<script>` built from data.

## URLs, redirects and links

- Validate any URL that reaches `href`, `src`, `action`, `formaction` or `window.open` against a scheme allow-list of `https:` (and `mailto:`/`tel:` where needed). `javascript:` and `data:` URLs execute.
- Build redirects from an allow-list of known paths, never from a raw query parameter — an open redirect on your own domain is a working phishing page.
- Put `rel="noopener noreferrer"` on `target="_blank"` links to third-party sites. Modern browsers imply `noopener`; `noreferrer` is what stops your internal URLs leaking in the referrer.

## Cross-origin messaging and embedding

- In every `message` listener, check `event.origin` against an exact expected origin before reading `event.data`, and treat the payload as untrusted input after that.
- Never call `postMessage(data, '*')` with anything sensitive; name the target origin.
- Sandbox embedded third-party content (`<iframe sandbox="...">`) and grant only the capabilities it needs.

## Sessions, tokens and authorisation

- With cookie sessions, cookies must be set server side with `HttpOnly; Secure; SameSite=Lax` (or `Strict`), and every state-changing request must carry the framework's CSRF token.
- Prefer keeping access tokens in memory over `localStorage`, which any injected script can read and which survives the tab. If a refresh flow is needed, the refresh token belongs in an `HttpOnly` cookie.
- Client-side permission checks are user experience only. Hiding a button is not access control; the server must enforce every rule the UI implies.
- Use `crypto.randomUUID()` or `crypto.getRandomValues()` for anything id- or nonce-like. `Math.random()` is predictable.

## Headers and third-party code

- Ship a Content-Security-Policy and keep it strict: no `unsafe-inline`, no `unsafe-eval`, explicit origins for scripts, connections and frames. Enable Trusted Types where the browsers you support allow it.
- Adding a third-party script is a security decision: check what it loads at runtime, give it Subresource Integrity where the URL is versioned, add its origins to the CSP, and record what data it collects.
- Do not disable CSP, CORS checks or certificate validation to make something work locally; use a local proxy or a development-only configuration that cannot ship.

## Data handling in the client

- Keep personal data out of `console` output, analytics events, error reports, breadcrumbs and URL query strings — all four leave the browser and are retained by someone.
- Do not persist personal or financial data in `localStorage`, `sessionStorage` or IndexedDB unless the feature requires it, and clear it on sign-out.
- Scrub tokens, cookies and request bodies before sending an error to monitoring.

Go deeper: `frontend-code-review` for the review checklist, and the stack's development skill for the framework-specific form of each rule.
