Browser-side hardening: XSS and safe text binding, per-sink URL policy, DOM clobbering, nonce-based CSP, Trusted Types, subresource integrity, iframe capability minimization, postMessage validation, and where client state may live. Use when generating HTML, JSX, Vue, or Svelte templates, setting response headers in a web app, embedding third-party scripts or frames, or storing anything client-side.
Installs into .claude/skills of the current project.
Are you the author of Frontend Security?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/shieldnet-360-frontend-security)
---
name: frontend-security
description: "Browser-side hardening: XSS and safe text binding, per-sink URL policy, DOM clobbering, nonce-based CSP, Trusted Types, subresource integrity, iframe capability minimization, postMessage validation, and where client state may live. Use when generating HTML, JSX, Vue, or Svelte templates, setting response headers in a web app, embedding third-party scripts or frames, or storing anything client-side."
---
<!-- Native skill bundle for agent-skills (cross-tool convention). Generated by `secure-vibe dev regenerate`. -->
<!-- Do not edit by hand; the source of truth is skills/frontend-security/SKILL.md. -->
# Frontend Security
Browser-side hardening: XSS and safe text binding, per-sink URL policy, DOM clobbering, nonce-based CSP, Trusted Types, subresource integrity, iframe capability minimization, postMessage validation, and where client state may live. Use when generating HTML, JSX, Vue, or Svelte templates, setting response headers in a web app, embedding third-party scripts or frames, or storing anything client-side.
## ALWAYS
- Render untrusted data through the framework's ordinary text interpolation, which escapes for the output context. Do not reach for a raw-HTML API merely to display user data. Where rich HTML genuinely must be rendered, put it through a maintained sanitizer (DOMPurify) with an explicit allowlist first.
- Validate URL-bearing attributes **per sink**, not against one global scheme list. A navigation target (`href`, `action`, `formaction`, an `<iframe src>`) and an image source do not share a trust model: `javascript:` is never acceptable anywhere, while a narrowly constrained `data:image/…` or `blob:` may be exactly what an image sink requires. Decide the permitted schemes for each sink and check against that list.
- Read security-sensitive configuration and control-flow values from lexical variables or an object you own — never from a named `window` / `document` property (`window.config`, `document.redirectTo`, an implicit global). The browser exposes elements by `id` and `name` as named properties, so injected markup can shadow the value your code expected. An explicit `getElementById()` lookup is not the problem; trusting an ambient global is. Type-check what you read before using it in a privileged way.
- Build the `Content-Security-Policy` around a **nonce or hash as the trust root**: `script-src 'nonce-{value}' 'strict-dynamic'; object-src 'none'; base-uri 'none'`. Keeping `'self'` beside the nonce means every same-origin script is still trusted — that is a host-allowlist policy wearing a nonce, which may be the right trade-off but is not a strict CSP, so do not call it one. Generate the nonce from a CSPRNG afresh for **every HTML response**, and apply it only to scripts the server itself authorizes.
- Where you deploy Trusted Types, enforce `require-trusted-types-for 'script'` **and** restrict which policies may exist with the `trusted-types` directive. The first makes DOM sinks demand a typed value; without the second, any code can mint a pass-through policy (`createHTML: s => s`) and the guarantee is gone. Keep policies few, named, and centrally reviewed.
- Load third-party scripts and stylesheets from **version-pinned, immutable** URLs with `integrity="sha384-…"` and `crossorigin="anonymous"`. A hash pinned against a mutable URL breaks the page the first time the provider ships a legitimate update, which is how integrity checks come to be quietly deleted. If a provider cannot offer stable bytes with CORS, self-host rather than drop SRI.
- Sandbox every `<iframe>`, starting from **no** capability tokens and adding only what the embedded content needs. Do not combine `allow-scripts` with `allow-same-origin`: together they let the framed document reach the parent and remove its own `sandbox` attribute, so the sandbox stops meaning anything. If the content genuinely needs both, isolate it on a separate origin instead.
- On `postMessage`, name an explicit `targetOrigin` when sending — never `*` — and on receipt verify `event.origin` against an allowlist, validate the message's shape before reading any field, and where the channel expects one particular frame, check `event.source` as well. Origin, schema and sender are three separate checks.
- Set `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin` or stricter, `Permissions-Policy` dropping unused features, and `Cross-Origin-Opener-Policy: same-origin` to sever the opener relationship at the document level. Remove framework version banners here too (`X-Powered-By`, a detailed `Server`). Note that `no-referrer-when-downgrade` is **weaker** than the current browser default — configuring it is a step backwards.
- Send HSTS on production HTTPS. Add `includeSubDomains` only after confirming every subdomain serves HTTPS, and `preload` only as a deliberate, hard-to-reverse commitment for the registrable domain: the token makes a domain *eligible* for the preload list, it does not enrol it, and reversing it is slow.
- Keep authentication secrets out of JavaScript-readable storage — no access tokens, refresh tokens, JWTs, or sensitive personal or business data in `localStorage` or `sessionStorage`, where a single XSS reads all of it — and clear authenticated client-side state on logout. Server-issued session cookies are the alternative: `Secure`, `HttpOnly`, an application-appropriate `SameSite`, and the `__Host-` prefix for host-only cookies at `Path=/`. `auth-security` owns issuing them; this rule exists so a frontend review recognises an auth value that has escaped into JavaScript's reach.
## NEVER
- Use `dangerouslySetInnerHTML`, `v-html`, `{@html …}`, `innerHTML =`, or `document.write` with untrusted input.
- Use `eval`, `new Function`, `setTimeout(string)`, or `setInterval(string)`.
- Read or write `document.cookie` from JavaScript for an auth cookie — it should be `HttpOnly`, which means JavaScript cannot see it and code that does is reaching for a cookie that was never hardened.
- Treat **tightening the HTML sanitizer** as the fix when the exploit rides on content the sanitizer allows **by design** — a valid link, an `<img src>`, a permitted attribute. The sanitizer is working. The vulnerable behaviour is downstream, where some sink turns that allowed value into a navigation, a command, or a native capability. Fix the sink and reduce its authority.
## KNOWN FALSE POSITIVES
- Internal admin tools rendering Markdown or rich text from trusted authors may use a raw-HTML API after a sanitizer pass; document the sanitizer call inline.
- A deliberately **sandboxed** browser-extension page may use a more permissive CSP for code that needs eval-like behaviour, provided it stays isolated from the extension APIs. That is not a licence to add `'unsafe-eval'` to MV3 `extension_pages` — Chrome rejects that policy outright, and WebAssembly has its own `'wasm-unsafe-eval'` token.
- A WebSocket to a non-same-origin endpoint where the server validates `Origin`.
- A native or WASM decoder that is merely registered or configured is not yet an attack surface. Before flagging one, establish a reachable runtime path from untrusted input to that decoder — including an implementation fetched lazily at runtime, which does not have to be in the shipped bundle to be reachable.
## Reference files
Read these only when the task calls for them.
- `references/browser-controls.md`
- `references/verifying-findings.md`