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-dab1ea0a)
---
id: frontend-security
version: "2.0.0"
title: "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."
category: prevention
severity: high
applies_to:
- "when generating HTML / JSX / Vue / Svelte templates"
- "when wiring up response headers in a web app"
- "when adding third-party script tags, CDN resources, or embedded frames"
- "when passing messages between windows, frames, or workers"
- "when storing state client-side"
languages: ["html", "javascript", "typescript", "tsx", "jsx", "vue", "svelte"]
token_budget:
minimal: 1650
compact: 2250
full: 2900
rules_path: "rules/"
related_skills: ["cors-security", "auth-security", "api-security"]
last_updated: "2026-08-12"
sources:
- "OWASP XSS Prevention Cheat Sheet"
- "OWASP Content Security Policy Cheat Sheet"
- "OWASP DOM Clobbering Prevention Cheat Sheet"
- "OWASP HTML5 Security Cheat Sheet"
- "W3C Trusted Types specification"
- "CWE-79: Improper Neutralization of Input During Web Page Generation"
- "CWE-346: Origin Validation Error"
---
# Frontend Security
## Rules (for AI agents)
### 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.
## Context (for humans)
Escaping is still the base layer and CSP is still what turns one missed escape into a
report instead of a stolen session. What has changed is where the browser now draws
its boundaries: opener isolation, cross-origin isolation, and Trusted Types are
document-level controls that a per-element attribute cannot replace.
Two failure shapes account for most of what goes wrong in review. The first is a
control that is present but hollow — a nonce alongside `'self'`, a sandbox with
`allow-scripts allow-same-origin`, Trusted Types enforced without restricting who may
create a policy. Each looks like the hardened version and grants what it appears to
withhold. The second is a defence recommended against an old default: browsers have
moved, and a `Referrer-Policy` copied from a 2019 guide now configures something
weaker than doing nothing at all.
The corollary for anything written here: prefer the browser's current default to a
remembered value, and when a rule names a header value or a token, check it against
the platform rather than against an example.
## References
- `references/verifying-findings.md` — confirm or refute a finding, then lock it
- `references/browser-controls.md` — per-framework text binding, strict versus
allowlist CSP, the response-header matrix, when COOP / COEP / CORP each apply, and
extension-CSP specifics
- `rules/csp_defaults.json`
- `rules/xss_sinks.json`
- [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).
- [OWASP CSP Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html).
- [OWASP DOM Clobbering Prevention](https://cheatsheetseries.owasp.org/cheatsheets/DOM_Clobbering_Prevention_Cheat_Sheet.html).
- [Trusted Types specification (W3C)](https://www.w3.org/TR/trusted-types/).
- [CWE-79](https://cwe.mitre.org/data/definitions/79.html) · [CWE-346](https://cwe.mitre.org/data/definitions/346.html).