For text content: use textContent / framework escaping ({value}) — no function needed. For attribute values: escape & < > ' ". For HTML rendering: DOMPurify with a strict allowlist (no script, on*, javascript: URIs). For URL attributes: validate scheme via URL constructor. For payment-specific (card numbers, CVV): strip non-digits, never log, mask in UI. Never roll your own HTML sanitizer with regex.
Category
Security
XSS, CSRF, CSP, cookies, auth flows, OWASP for frontend.
20 questions
Default: let the framework escape ({value} in React, {{value}} in Vue/Angular). Never use dangerouslySetInnerHTML/v-html/innerHTML with untrusted content unless sanitized through DOMPurify with a strict allowlist. Validate URL schemes (block javascript:) before rendering as href/src. Never eval user input. Use CSP as defense-in-depth. For rich-text editors, sanitize at storage (not just display) and consider Trusted Types in Chromium.
React's `{value}` auto-escapes — that's your default. The XSS surface in React is `dangerouslySetInnerHTML`, attacker-controlled URLs in `href`/`src`, `eval`/`new Function`/`setTimeout(string)`, and direct DOM manipulation via refs. Sanitize HTML with DOMPurify, validate URL schemes, ban `eval`-family, and add a strict Content-Security-Policy as a defense-in-depth.
Default to text rendering (`textContent`, React's `{value}`) — never `innerHTML` with untrusted input. When you must render HTML, sanitize with DOMPurify on the way *in* and use a strict allow-list. Combine with a Content-Security-Policy that bans inline scripts and `eval`. Treat user-supplied URLs as suspicious — block `javascript:` and `data:` for href/src.
XSS (cross-site scripting): attacker injects script that runs in user's browser. Defend by escaping output (frameworks do it for you), strict Content-Security-Policy, HttpOnly cookies (script can't read), avoid innerHTML/dangerouslySetInnerHTML with user input, sanitize HTML with DOMPurify when you must. CSRF (cross-site request forgery): attacker tricks user's browser into making authenticated requests. Defend with SameSite=Lax cookies, CSRF tokens (double-submit cookie), origin/sec-fetch-site validation. They're separate threats with separate defenses.
CSRF = attacker tricks the user's browser into sending an authenticated request to your site. Defense: SameSite cookies (Lax/Strict), a CSRF token verified server-side for state-changing requests, and `Origin`/`Referer` checks. Token refresh: short-lived access token (~15min) + long-lived refresh token (HttpOnly cookie, rotated on each use, server-side revocable). On 401, hit /refresh, get a new access token, retry the request — with a mutex so concurrent requests don't trigger multiple refreshes.
Don't store auth tokens in localStorage — any JS on the origin (XSS, malicious extension) can read them. Use **httpOnly + Secure + SameSite=Lax/Strict cookies** so the browser sends them automatically but JS can't read. Add CSRF protection (double-submit or SameSite). Keep tokens short-lived; refresh server-side; rotate on suspicious activity.
localStorage and sessionStorage are accessible from JS — XSS readable, no HttpOnly equivalent. Never store: auth tokens (use HttpOnly cookies), passwords, credit cards, encryption keys, PII without consent. OK to store: UI prefs (theme, language), drafts (low-sensitivity), feature flag overrides, public metadata. For sensitive client-side persistence use IndexedDB with same caveats + consider encryption with key derived per-user. Clear on logout. Scope by user when shared device is possible.
HttpOnly cookies aren't readable by JS — XSS can't exfiltrate the token. Combine with Secure, SameSite, and a CSRF strategy.
`target='_blank'` opens a new tab/window where the new page gets a `window.opener` reference back to your page — letting it run `window.opener.location = 'phishing-url'` (reverse tabnabbing). `rel='noopener'` blocks that. `rel='noreferrer'` additionally strips the `Referer` header. Modern browsers add `noopener` implicitly, but the rel attribute remains best practice.
Inline scripts blocked: any <script>...</script> in HTML, javascript: URIs, inline event handlers (onclick='...'), eval/new Function — all fail silently. Strict CSP needs nonces or hashes for legitimate inline. frame-src blocked: any iframe (payment widgets, embedded videos, OAuth popups, third-party SSO) won't load. For embeds, host them on an allowed domain or coordinate with the merchant to add your domain to frame-src and script-src. Test with the merchant's CSP enabled in staging.
Session cookie: random opaque ID, server holds the truth, revoke instantly by deleting the session. JWT: signed token carries claims, stateless — server doesn't need to look anything up. Sessions win for typical web apps (revocation, security, simplicity). JWTs win for microservice-to-microservice auth and short-lived access tokens paired with refresh tokens. Storing JWTs in localStorage is the most common XSS footgun.
Authentication = who you are (login, tokens/sessions); authorization = what you can do (roles/permissions). Store tokens safely (httpOnly cookies > localStorage), refresh them, guard routes, hide unauthorized UI — but the real enforcement is always server-side. The frontend only reflects auth state.
Short-lived access token + long-lived refresh token. Store them safely (httpOnly cookies preferred over localStorage). On a 401, use the refresh token to get a new access token transparently, queueing in-flight requests; if refresh fails, log out. Never trust the client — the server validates the token.
Frontend RBAC is UX, not security. Hide unauthorized UI but treat the server as the only authority. Encode permissions as capabilities (`can('edit:post', resource)`), not raw role names, so policies can evolve.
Minimize what reaches the client, never store secrets in localStorage, use HttpOnly+Secure+SameSite cookies for tokens, enforce HTTPS, prevent XSS (sanitize, CSP, framework escaping), avoid logging sensitive data, mask in the UI, and remember the client is never a trust boundary.
Layered defense: HTTPS + HSTS, secure cookies (HttpOnly+Secure+SameSite), Content-Security-Policy (script-src nonces, no unsafe-inline), framework auto-escape + DOMPurify for HTML, CSRF tokens or origin check on state-changing requests, parameterized queries (SQL/NoSQL), input validation at boundaries (Zod), per-endpoint rate limit, secrets in env (never in code), dependency scanning (npm audit, Dependabot), least-privilege auth scopes, monitoring + alerting on anomalies. OWASP Top 10 is the canonical checklist.
Never trust client-side events for money. The browser-side defense is origin-checking postMessage and framing protections, but the real answer: payment success must be confirmed server-to-server (webhook / verify call), not via a postMessage the parent can forge.
Proxy via server (never API key in browser), use streaming (SSE) for responsive UX, schema-validate structured outputs (Zod), sanitize any HTML output (DOMPurify), handle rate-limit/timeout/error with retry+backoff+circuit-breaker, log redacted, enforce per-user budget. For OpenAI specifically: stream:true, structured outputs via JSON schema mode, function calling for tool use. Treat AI responses as untrusted input — never eval, never render raw HTML.
Treat AI calls like any third-party API + extra care for data sensitivity. Don't send PII/secrets to LLMs unless contracted; strip or tokenize first. Use server-side proxy (never API key in browser). Set retention/no-train flags on provider APIs. Validate output before rendering (LLMs hallucinate URLs, code, HTML — sanitize and never eval). Rate-limit and budget per user/session. Log redacted inputs for audit. For sensitive domains (health/finance), use providers with HIPAA/SOC2 + data processing agreements.