Claude Fable 5.1 & GPT-6 Astra packages are live

Xss

Free

Preventing cross-site scripting through contextual output encoding, a strict Content-Security-Policy, and safe DOM and framework APIs.

221 lines8.1 KB Deepseek Security
targetModels
DeepSeek V4DeepSeek V3.2DeepSeek R1DeepSeek V3 FamilyFuture DeepSeek Models
name
xss
category
Security
description
Preventing cross-site scripting through contextual output encoding, a strict Content-Security-Policy, and safe DOM and framework APIs.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for DeepSeek: scripts/model-profiles.json -->

#Task boundary

  1. Implement exactly the task as stated. Do not add abstractions, options, config, or files the task did not name.
  2. Comments, identifiers, commit messages and log strings are English only.
  3. Stop when the checklist at the end passes. Do not refactor or "improve" surrounding code.
  4. Every checklist item below is backed by an assertion in a test or by pasted command output, never by a sentence.

#Purpose

Rules for stopping attacker-controlled data from executing as script in a user's browser. Covers stored, reflected and DOM-based XSS.

The rule underneath everything: encode for the context the data lands in. There is no single "escape" function, because HTML, attributes, JavaScript, URLs and CSS have different metacharacters.


#Output encoding by context

The same value needs different treatment depending on where it is inserted.

ContextExampleEncode
HTML body<p>HERE</p>& < > → entities
Attribute value<div title="HERE">Above plus " and '; always quote
URL parameter<a href="/s?q=HERE">encodeURIComponent
JavaScript string<script>var x="HERE"</script>Do not. Pass via JSON.parse from a data attribute
CSS valuestyle="width:HERE"Do not. Use an allow-list of known values

Never insert untrusted data into a <script> block, an inline event handler (onclick=), a javascript: URL, or inside <style>. These are execution contexts where no encoding is reliable. Pass data through a <script type="application/json"> block or a data- attribute and read it with JSON.parse.

html
<!-- Safe: the value is data, parsed explicitly, never evaluated -->
<div id="cfg" data-user='{"name":"…"}'></div>
<script>
  const cfg = JSON.parse(document.getElementById("cfg").dataset.user);
</script>

#DOM APIs

The API you choose decides whether a string can become markup.

js
// DANGEROUS — parses HTML, executes injected handlers
el.innerHTML = userInput;
el.outerHTML = userInput;
el.insertAdjacentHTML("beforeend", userInput);
document.write(userInput);

// SAFE — the value is always text, never parsed
el.textContent = userInput;
el.setAttribute("title", userInput);
el.append(document.createTextNode(userInput));

textContent is the default. Reach for innerHTML only when rendering markup is the actual requirement, and then only after sanitising.

Never pass untrusted input to eval, new Function, setTimeout/ setInterval as a string, or element.setAttribute("on*", …). Each is a direct path from string to execution.

Never assign untrusted input to href or src without scheme validation — a javascript: URL executes on click:

js
const url = new URL(input, location.origin);
if (!["http:", "https:"].includes(url.protocol)) throw new Error("blocked scheme");
a.href = url.href;

#Sanitising when HTML is required

When users must submit rich text, sanitise with a maintained, allow-list-based library. Do not write your own.

js
import DOMPurify from "dompurify";

el.innerHTML = DOMPurify.sanitize(userHtml, {
  ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "ul", "ol", "li", "code"],
  ALLOWED_ATTR: ["href", "title"],
});

Sanitise on output, in the browser, immediately before insertion — or on both input and output. Sanitising only on input is fragile: the stored value survives a library upgrade, a changed rendering path, or a second consumer that never sanitises.

Never deny-list tags (strip <script>). Bypasses are endless: <img onerror>, <svg onload>, <iframe srcdoc>, malformed nesting, mutation XSS. Allow-list only.


#Framework escape hatches

Modern frameworks encode by default. Every XSS in a React or Vue app is therefore in a named escape hatch — audit these specifically:

FrameworkDangerous API
ReactdangerouslySetInnerHTML
Vuev-html
AngularbypassSecurityTrustHtml, [innerHTML]
Svelte{@html …}
SolidinnerHTML prop

The React name is deliberate. Treat every occurrence as requiring a sanitiser and a comment explaining why raw HTML is necessary.

Angular's DomSanitizer bypass methods disable the framework's protection entirely — bypassSecurityTrustHtml on user input is equivalent to innerHTML.


#Content-Security-Policy

CSP is the layer that limits damage when encoding fails. It is not a substitute for encoding.

csharp
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
  frame-ancestors 'none';
  require-trusted-types-for 'script'
  1. 'nonce-…' with 'strict-dynamic' is the modern strict policy. The nonce must be CSPRNG-generated per response and never reused.
  2. object-src 'none' kills plugin-based execution.
  3. base-uri 'none' stops <base> injection redirecting relative script URLs.
  4. frame-ancestors 'none' prevents clickjacking; it supersedes X-Frame-Options.
  5. require-trusted-types-for 'script' makes DOM-XSS sinks throw unless the value passed a Trusted Types policy — the strongest available control against DOM-based XSS.

Never ship script-src 'unsafe-inline' or 'unsafe-eval'. Together they disable most of what CSP is for. Never use a host allow-list alone — hosted JSONP endpoints and outdated libraries on a permitted CDN defeat it.

Deploy with Content-Security-Policy-Report-Only and a report-to endpoint first, fix the violations, then enforce.


#Cookies and related headers

  1. Session cookies carry HttpOnly so that XSS cannot read them. This does not prevent XSS; it limits the payoff.
  2. X-Content-Type-Options: nosniff stops the browser reinterpreting a response as HTML. A user-uploaded file served without it can become a stored XSS.
  3. Serve user uploads from a separate origin, so injected content cannot reach your cookies or DOM.
  4. Set an explicit Content-Type with charset=utf-8. Charset confusion has historically enabled encoding bypasses.

#Anti-patterns

Anti-patternWhy it failsFix
el.innerHTML = inputParses and executes markupel.textContent
Stripping <script> tags<img onerror>, <svg onload>, mutation XSSAllow-list sanitiser
One escapeHtml() for every contextAttribute, URL and JS contexts differEncode per context
Sanitising only on inputSurvives library and render-path changesSanitise on output
script-src 'unsafe-inline'Disables the protection CSP exists forPer-response nonce
href = input uncheckedjavascript: executes on clickValidate the scheme
dangerouslySetInnerHTML with raw inputBypasses framework encodingSanitise, or don't
Uploads served from the app originStored XSS with full cookie accessSeparate origin, nosniff

#Checklist

  • Output is encoded for its specific context, not a single generic escape
  • No untrusted data inside <script>, <style>, on* handlers or javascript:
  • textContent used by default; innerHTML only with a sanitiser
  • Rich text passes an allow-list sanitiser at output time
  • Every framework escape hatch is audited and justified in a comment
  • href and src values are scheme-validated
  • CSP set with a per-response nonce and strict-dynamic
  • No 'unsafe-inline' or 'unsafe-eval' in script-src
  • object-src 'none', base-uri 'none', frame-ancestors 'none' present
  • Session cookies are HttpOnly; responses carry nosniff
  • User uploads are served from a separate origin