DOM Security Hardening
Purpose
XSS attacks kill applications. This skill hardens the DOM attack surface by enforcing a strict Content Security Policy, eliminating unsafe DOM APIs, and stripping execution vectors like inline scripts and styles. Following this skill is MANDATORY for any user-facing web application handling user input.
When to use
- Setting up the initial
index.html or document root of a web application
- Refactoring legacy code that relies on direct DOM manipulation
- Auditing a frontend codebase for XSS vulnerabilities
- Processing rich text or markdown input from untrusted users
- Creating forms or chat systems that accept user input
When NOT to use
- CSS framework design (separate concern)
- React component prop validation (use React Component Design)
- Backend input validation (different layer, still required)
Inputs required
- HTML document structure
- JavaScript files with DOM manipulation
- Understanding of Content Security Policy basics
- Sanitizer library decision (DOMPurify, sanitize-html, etc.)
Workflow
- Enforce CSP: Add strict
<meta http-equiv="Content-Security-Policy"> tag in HTML <head> (or HTTP headers)
- Externalize Assets: Extract ALL inline
<script> and <style> blocks into separate .js and .css files
- Remove Inline Events: Replace inline
onclick="...", onchange="..." with standard addEventListener bindings
- Replace Unsafe DOM: Replace
innerHTML, outerHTML, dangerouslySetInnerHTML with textContent or innerText
- Implement Sanitizer: If HTML rendering is REQUIRED, use DOMPurify or equivalent BEFORE insertion
- Test CSP: Verify policy blocks all unauthorized execution attempts
- Verify No Bypasses: Use security scanner (OWASP ZAP, Burp) to confirm XSS vectors are closed
Rules
- MUST implement strict CSP:
default-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'
- MUST NEVER use inline
<script> blocks in HTML
- MUST NEVER use inline
style="..." attributes
- MUST NEVER use
eval(), setTimeout(string), new Function(string)
- MUST NEVER use
innerHTML, outerHTML, or dangerouslySetInnerHTML with user input
- MUST use
textContent or innerText for all dynamic text insertions
- MUST sanitize user HTML with DOMPurify before any insertion
- MUST NOT use
'unsafe-inline' or 'unsafe-eval' in CSP
Anti-patterns
innerHTML Assignment: element.innerHTML = userInput (immediate XSS)
javascript: URIs: href="javascript:void(0)" or href="javascript:alert(1)"
- Unsafe CSP:
Content-Security-Policy: default-src *; script-src 'unsafe-inline'
- Event Handler Strings: Creating event handlers from user input or strings
- Trusting User Input: Assuming any user input is safe to insert into DOM
- Missing Sanitizer: Using rich text editor without sanitizing output
Failure conditions
- CSP header/meta tag is missing
- Inline scripts or styles remain in production code
innerHTML used with user input
dangerouslySetInnerHTML used without sanitization
- CSP allows
'unsafe-inline' or 'unsafe-eval'
- Sanitizer library is not installed for rich text rendering
Validation checklist
Output format
- HTML file: Strict CSP meta tag in
<head>, external <script> tags at end of <body>
- JavaScript: All DOM mutations via safe APIs (
textContent, className, setAttribute)
- CSS: Separate
.css files, no inline styles anywhere
- Rich Text: HTML sanitized via DOMPurify before insertion
- Validation: Automated XSS scan passes
Security considerations
- Threat Model: Prevent XSS attacks via user input injection, DOM gadgets, third-party scripts
- Mitigations: CSP prevents execution, sanitizer prevents HTML injection, safe APIs prevent eval
- Constraints: CSP may conflict with analytics/ads (allow specific domains only)
- Legacy Code: Some frameworks may require CSP relaxation (document trade-offs)
- Third-party Scripts: Load analytics/ads only from trusted CDNs with subresource integrity (SRI)
Agent execution notes
- Agent MAY: Add CSP header, externalize inline scripts/styles, replace innerHTML with textContent, implement DOMPurify
- Agent MUST NEVER: Use
'unsafe-inline' or 'unsafe-eval' in CSP, ignore sanitization requirements, leave inline event handlers
- Agent MUST ASK: Before adding third-party scripts, before relaxing CSP for legacy code, before using dangerouslySetInnerHTML
- Agent MUST VALIDATE: CSP policy is strict, no inline scripts remain, no unsafe DOM APIs, XSS scanner passes
Example
❌ Anti-pattern (Unsafe XSS vectors, no CSP):
<!-- No CSP -->
<a href="javascript:void(0)" me</a>
<div id="user-bio"></div>
<script>
// Unsafe: direct user input to DOM
document.getElementById('user-bio').innerHTML = getUserInput();
// Unsafe: inline style
document.getElementById('user-bio').setAttribute('style', 'color: red;');
// Unsafe: event handler string
setTimeout("console.log('vulnerable')", 1000);
</script>
✅ Correct pattern (Hardened, safe):
<!-- Strict CSP enforced -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; img-src 'self' https:; font-src 'self';"
>
<a href="#" id="submit-btn">Click me</a>
<div id="user-bio"></div>
<!-- External script only -->
<script src="/js/app.js"></script>
// app.js
// 1. Safe event binding
document.getElementById('submit-btn').addEventListener('click', (e) => {
e.preventDefault();
submitForm();
});
// 2. Safe text insertion (escapes HTML by default)
document.getElementById('user-bio').textContent = getUserInput();
// 3. Safe styling via CSS classes
document.getElementById('user-bio').classList.add('text-red');
// 4. If HTML rendering is absolutely required, sanitize first
import DOMPurify from 'dompurify';
document.getElementById('user-bio').innerHTML = DOMPurify.sanitize(getUserMarkdown());
1---2name: dom-security-hardening3description: When hardening a web application against Cross-Site Scripting (XSS) and injection attacks.4license: MIT5---67# DOM Security Hardening89## Purpose10XSS attacks kill applications. This skill hardens the DOM attack surface by enforcing a strict Content Security Policy, eliminating unsafe DOM APIs, and stripping execution vectors like inline scripts and styles. Following this skill is MANDATORY for any user-facing web application handling user input.1112## When to use13- Setting up the initial `index.html` or document root of a web application14- Refactoring legacy code that relies on direct DOM manipulation15- Auditing a frontend codebase for XSS vulnerabilities16- Processing rich text or markdown input from untrusted users17- Creating forms or chat systems that accept user input1819## When NOT to use20- CSS framework design (separate concern)21- React component prop validation (use React Component Design)22- Backend input validation (different layer, still required)2324## Inputs required25- HTML document structure26- JavaScript files with DOM manipulation27- Understanding of Content Security Policy basics28- Sanitizer library decision (DOMPurify, sanitize-html, etc.)2930## Workflow311. **Enforce CSP**: Add strict `<meta http-equiv="Content-Security-Policy">` tag in HTML `<head>` (or HTTP headers)322. **Externalize Assets**: Extract ALL inline `<script>` and `<style>` blocks into separate `.js` and `.css` files333. **Remove Inline Events**: Replace inline `onclick="..."`, `onchange="..."` with standard `addEventListener` bindings344. **Replace Unsafe DOM**: Replace `innerHTML`, `outerHTML`, `dangerouslySetInnerHTML` with `textContent` or `innerText`355. **Implement Sanitizer**: If HTML rendering is REQUIRED, use DOMPurify or equivalent BEFORE insertion366. **Test CSP**: Verify policy blocks all unauthorized execution attempts377. **Verify No Bypasses**: Use security scanner (OWASP ZAP, Burp) to confirm XSS vectors are closed3839## Rules40- MUST implement strict CSP: `default-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'`41- MUST NEVER use inline `<script>` blocks in HTML42- MUST NEVER use inline `style="..."` attributes43- MUST NEVER use `eval()`, `setTimeout(string)`, `new Function(string)`44- MUST NEVER use `innerHTML`, `outerHTML`, or `dangerouslySetInnerHTML` with user input45- MUST use `textContent` or `innerText` for all dynamic text insertions46- MUST sanitize user HTML with DOMPurify before any insertion47- MUST NOT use `'unsafe-inline'` or `'unsafe-eval'` in CSP4849## Anti-patterns50- **`innerHTML` Assignment**: `element.innerHTML = userInput` (immediate XSS)51- **`javascript:` URIs**: `href="javascript:void(0)"` or `href="javascript:alert(1)"`52- **Unsafe CSP**: `Content-Security-Policy: default-src *; script-src 'unsafe-inline'`53- **Event Handler Strings**: Creating event handlers from user input or strings54- **Trusting User Input**: Assuming any user input is safe to insert into DOM55- **Missing Sanitizer**: Using rich text editor without sanitizing output5657## Failure conditions58- CSP header/meta tag is missing59- Inline scripts or styles remain in production code60- `innerHTML` used with user input61- `dangerouslySetInnerHTML` used without sanitization62- CSP allows `'unsafe-inline'` or `'unsafe-eval'`63- Sanitizer library is not installed for rich text rendering6465## Validation checklist66- [ ] CSP header/meta tag present with restrictive policy67- [ ] No `<script>` tags with inline code (all external)68- [ ] No inline `style="..."` attributes (all CSS classes)69- [ ] No `onclick`, `onchange`, `oninput` inline event handlers70- [ ] All DOM text insertions use `textContent` or `innerText`71- [ ] No `innerHTML`, `outerHTML`, or `dangerouslySetInnerHTML` with user input72- [ ] DOMPurify (or equivalent) used for any rich text rendering73- [ ] No `eval()`, `setTimeout(string)`, `new Function(string)` calls74- [ ] Security scanner passes (no XSS vulnerabilities detected)75- [ ] CSP blocks inline script execution (verify in browser console)7677## Output format78- **HTML file**: Strict CSP meta tag in `<head>`, external `<script>` tags at end of `<body>`79- **JavaScript**: All DOM mutations via safe APIs (`textContent`, `className`, `setAttribute`)80- **CSS**: Separate `.css` files, no inline styles anywhere81- **Rich Text**: HTML sanitized via DOMPurify before insertion82- **Validation**: Automated XSS scan passes8384## Security considerations85- **Threat Model**: Prevent XSS attacks via user input injection, DOM gadgets, third-party scripts86- **Mitigations**: CSP prevents execution, sanitizer prevents HTML injection, safe APIs prevent eval87- **Constraints**: CSP may conflict with analytics/ads (allow specific domains only)88- **Legacy Code**: Some frameworks may require CSP relaxation (document trade-offs)89- **Third-party Scripts**: Load analytics/ads only from trusted CDNs with subresource integrity (SRI)9091## Agent execution notes92- Agent MAY: Add CSP header, externalize inline scripts/styles, replace innerHTML with textContent, implement DOMPurify93- Agent MUST NEVER: Use `'unsafe-inline'` or `'unsafe-eval'` in CSP, ignore sanitization requirements, leave inline event handlers94- Agent MUST ASK: Before adding third-party scripts, before relaxing CSP for legacy code, before using dangerouslySetInnerHTML95- Agent MUST VALIDATE: CSP policy is strict, no inline scripts remain, no unsafe DOM APIs, XSS scanner passes9697## Example9899**❌ Anti-pattern (Unsafe XSS vectors, no CSP):**100```html101<!-- No CSP -->102<a href="javascript:void(0)" onclick="submitForm()">Click me</a>103<div id="user-bio"></div>104105<script>106 // Unsafe: direct user input to DOM107 document.getElementById('user-bio').innerHTML = getUserInput();108 109 // Unsafe: inline style110 document.getElementById('user-bio').setAttribute('style', 'color: red;');111 112 // Unsafe: event handler string113 setTimeout("console.log('vulnerable')", 1000);114</script>115```116117**✅ Correct pattern (Hardened, safe):**118```html119<!-- Strict CSP enforced -->120<meta http-equiv="Content-Security-Policy"121 content="default-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; img-src 'self' https:; font-src 'self';"122>123124<a href="#" id="submit-btn">Click me</a>125<div id="user-bio"></div>126127<!-- External script only -->128<script src="/js/app.js"></script>129```130131```javascript132// app.js133134// 1. Safe event binding135document.getElementById('submit-btn').addEventListener('click', (e) => {136 e.preventDefault();137 submitForm();138});139140// 2. Safe text insertion (escapes HTML by default)141document.getElementById('user-bio').textContent = getUserInput();142143// 3. Safe styling via CSS classes144document.getElementById('user-bio').classList.add('text-red');145146// 4. If HTML rendering is absolutely required, sanitize first147import DOMPurify from 'dompurify';148document.getElementById('user-bio').innerHTML = DOMPurify.sanitize(getUserMarkdown());149```