XSS Prevention skill for Codex
Encode for the sink, not for the input. Codex searches the codebase for every place auto-escaping is bypassed, from v-html and {@html} to html_safe and Go text/template, traces each value back to its source, and applies the encoding that matches the actual output context. User HTML and Markdown get an allowlist sanitiser applied after rendering, URL attributes get scheme checks, and payload tests lock each fix in.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- xss-prevention Installs to ~/.agents/skills/xss-prevention/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install xss-prevention The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project xss-prevention Installs to .agents/skills/xss-prevention/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/xss-prevention.zipunzip -o xss-prevention.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) xss-prevention PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $xss-prevention, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Prevents cross-site scripting by keeping framework auto-escaping intact, encoding for the exact output context (HTML, attribute, URL, JavaScript, CSS), sanitizing user HTML and Markdown with DOMPurify or nh3, and enforcing Trusted Types. Finds escape hatches such as dangerouslySetInnerHTML, v-html, {@html}, |safe, html_safe and template.HTML. Use when rendering user content, reviewing templates or React components for XSS, fixing a reported XSS, or adding rich text or Markdown.
Use it when
- Rendering user-supplied text, HTML, Markdown or URLs
- Fixing a reported reflected, stored or DOM XSS
- Reviewing templates or components that bypass auto-escaping
- Adding a rich text editor or comment system
Use something else when
- You are writing or rolling out the CSP header itself: use content-security-policy
- You want a full security review of a diff, not just XSS: use secure-code-review
How to check the result
Regression tests render each fixed component with img-onerror, svg-onload, javascript: URL and script-breakout payloads and assert no executable element or attribute survives.
The mistake it prevents
Escaping once at input for HTML and then placing the value in a script block or href, where HTML escaping does not stop execution.
Done well means: untrusted data never reaches an HTML, script or URL sink without the encoding or sanitisation that matches that exact context, every escape hatch in the codebase is justified in a comment, and a CSP or Trusted Types policy would catch the next mistake.
Inspect first
- Read
AGENTS.md; identify the template engine(s) and frontend framework. - Find every place auto-escaping is bypassed. These are the review list:
rg -n "dangerouslySetInnerHTML|v-html|\{@html|\[innerHTML\]|bypassSecurityTrust|innerHTML\s*=|outerHTML|insertAdjacentHTML|document\.write|\.html\(" -g '!node_modules' -g '!dist'
rg -n "\|\s*safe|mark_safe|Markup\(|autoescape\s+(off|false)|html_safe|raw\(|\{!!|template\.HTML|template\.JS|template\.URL|th:utext|Html\.Raw" -g '!node_modules'
rg -n "text/template" --type go # text/template does no HTML escaping
rg -n "href=\{|:href=|\[href\]|setAttribute\(.(href|src)" -g '!node_modules'
- For each hit, trace the value back to its source. Data from users, URLs, headers, third-party APIs, LLM output and your own database (stored XSS) is all untrusted.
Context decides the encoding
| Sink | Correct handling |
|---|---|
| HTML text node | Framework auto-escaping ({value}, {{ value }}) |
| Quoted HTML attribute | Auto-escaping, attribute always quoted |
href, src, action, formaction | Parse the URL and allow only http:, https:, mailto: (and relative); then escape |
Inside <script> | Do not interpolate. Serialise to JSON in a data block (Django json_script, JSON.stringify with < escaped to <) and parse it |
Event handler attribute (onclick=) | Never put data here; attach listeners in JS |
style attribute or <style> | Avoid; use classes or set element.style.prop from JS |
| DOM via JS | textContent, setAttribute for non-URL attributes, createElement |
| User-supplied HTML (rich text, Markdown output) | Sanitise with an allowlist sanitiser, then insert |
Escaping for HTML is wrong inside JavaScript strings and vice versa. The context determines the encoder; “escape everything once at input” fails in at least one sink.
Framework escape hatches
| Framework | Hatch | Safe alternative |
|---|---|---|
| React | dangerouslySetInnerHTML, href={userUrl} | Render as text; sanitise and validate URL scheme. React 19 blocks javascript: URLs in some props but do not rely on it |
| Vue | v-html, dynamic :href | Text interpolation; sanitise first |
| Angular | bypassSecurityTrustHtml/Url/... | Let Angular’s sanitiser run; never bypass with user data |
| Svelte | {@html} | Sanitise first |
| Django / Jinja2 | ` | safe, mark_safe, Markup, autoescape off` |
| Rails ERB | raw, html_safe, <%== | sanitize helper with an explicit tag list, content_tag |
| Laravel Blade | {!! !!} | {{ }} |
| Go | text/template, template.HTML(...) | html/template, no type conversions on user data |
| Thymeleaf | th:utext | th:text |
Sanitising user HTML
Browser side, DOMPurify:
import DOMPurify from "dompurify";
const clean = DOMPurify.sanitize(userHtml, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "ul", "ol", "li", "code", "pre", "blockquote"],
ALLOWED_ATTR: ["href", "title"],
ALLOW_DATA_ATTR: false,
});
DOMPurify.addHook("afterSanitizeAttributes", (node) => {
if (node.tagName === "A") { node.setAttribute("rel", "noopener noreferrer"); node.setAttribute("target", "_blank"); }
});
Server side: Python nh3 (Rust ammonia bindings; bleach is deprecated), Ruby
Rails::HTML5::SafeListSanitizer or sanitize gem, Java OWASP HTML Sanitizer, Go
bluemonday, PHP HTML Purifier. Node server side: DOMPurify with jsdom, or
sanitize-html.
Rules:
- Sanitise the final HTML, after Markdown rendering, not the Markdown source. Markdown
allows raw HTML and
[x](javascript:...)links unless configured otherwise. - Sanitise at render time (or re-sanitise on read) so a sanitiser upgrade protects old rows.
- Do not post-process sanitised HTML with string replacement or a different parser; parser differentials reintroduce XSS (mutation XSS).
- SVG and MathML are where sanitiser bypasses cluster; leave them out of the allowlist unless needed.
Trusted Types
Trusted Types make DOM sinks such as innerHTML reject plain strings, so only values
produced by a reviewed policy reach them. Supported in Chromium browsers and being added
elsewhere; treat it as enforcement where available, not the only defence.
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types dompurify app-html
const appHtml = window.trustedTypes?.createPolicy("app-html", {
createHTML: (s) => DOMPurify.sanitize(s, { RETURN_TRUSTED_TYPE: false }),
});
el.innerHTML = appHtml ? appHtml.createHTML(input) : DOMPurify.sanitize(input);
Roll out with Content-Security-Policy-Report-Only first; violation reports list every
sink still receiving strings. CSP rollout details live in $content-security-policy.
Workflow
- [ ] 1. Run the escape-hatch searches; record each hit with file:line
- [ ] 2. Trace each to its source; mark trusted constant vs untrusted
- [ ] 3. Fix: remove the hatch, or sanitise / validate scheme, with a comment
- [ ] 4. Check URL-bearing attributes for javascript: and data: schemes
- [ ] 5. Check JSON-in-HTML and server-side rendering of state (__NEXT_DATA__-style blobs)
- [ ] 6. Add regression tests with payloads per context
- [ ] 7. Add or tighten CSP / Trusted Types in report-only mode
Verification
Add tests that render each fixed component or template with hostile input and assert the output is inert:
const payloads = [
`<img src=x onerror=alert(1)>`,
`"><svg onload=alert(1)>`,
`javascript:alert(1)`,
`</script><script>alert(1)</script>`,
`<a href="javascript:alert(1)">x</a>`,
];
for (const p of payloads) {
render(<Comment body={p} website={p} />);
expect(document.querySelector("img[onerror], svg[onload], script")).toBeNull();
expect(document.querySelector('a[href^="javascript"]')).toBeNull();
}
Then load the page in a local browser with each payload stored and confirm no dialog appears and the browser console shows no CSP violations from your own code. Never test payloads against systems the user does not own or is not authorised to test.
Output template
## XSS review: <scope>
| # | Location | Sink / context | Source | Status | Fix |
|---|---|---|---|---|---|
| 1 | web/Comment.tsx:22 | dangerouslySetInnerHTML | comment.body (DB, user) | Fixed | DOMPurify, tag allowlist |
Escape hatches remaining (justified): <list with reason>
Defence in depth: CSP <state>, Trusted Types <state>
Gotchas
encodeURIComponentdoes not make a URL safe forhref;javascript:survives it.- Stored data rendered in an admin panel is the most common high-impact XSS: admins see every user’s input.
- LLM output rendered as Markdown or HTML is untrusted input; prompt injection can make it emit payloads.
- Error pages and 404s that echo the path or query string are classic reflected XSS.
X-XSS-Protectiondoes nothing useful in current browsers; do not count it.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "XSS Prevention"
short_description: "Encode for the sink, not for the input"
default_prompt: "$xss-prevention Find every dangerouslySetInnerHTML in this React app and make it safe" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.