# 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.

- Skill: `shieldnet-360/frontend-security` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add shieldnet-360/frontend-security`
- Raw SKILL.md: https://api.skillmd.com/api/skills/shieldnet-360/frontend-security/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: ShieldNet-360 (https://skillmd.com/u/shieldnet-360)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/shieldnet-360/frontend-security

---


# 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).

