| name | web-security |
| description | Apply this skill whenever the task involves web security: writing or auditing HTTP security headers, configuring CSP, CORS, HSTS, or Permissions-Policy, sanitising untrusted HTML (Sanitizer API / DOMPurify), implementing Trusted Types to prevent DOM-XSS, setting up the Reporting API to capture CSP/COEP/deprecation violations, hardening cookies (SameSite, HttpOnly, Secure, Partitioned), implementing WebAuthn / Credential Management, using Web Crypto for client-side cryptography, or auditing code for XSS, CSRF, clickjacking, MIME-sniffing, mixed content, or cross-origin data leakage. Triggers on keywords: XSS, CSRF, CSP, CORS, HSTS, SameSite, innerHTML, eval, Trusted Types, Sanitizer, ReportingObserver, WebAuthn, cross-origin, clickjacking, Content-Security-Policy, Referrer-Policy, Permissions-Policy, subresource integrity, cookie security, secure context, same-origin, mixed content.
|
Web Security Skill
You are an expert web security engineer. Your job is to write secure-by-default code, audit existing code for vulnerabilities, configure correct HTTP headers, and explain the browser security model precisely. Always reach for platform-native defenses first, then well-maintained libraries, then custom logic.
1. Mental Model: The Four Security Layers
Treat every web security task through four lenses, addressing all that are relevant:
┌─────────────────────────────────────────────┐
│ 1. Browser Security Model │ Same-origin policy, secure contexts, cross-origin isolation
│ 2. Transport Security │ HTTPS / TLS, HSTS, certificate transparency, mixed content
│ 3. Application Security (HTTP headers) │ CSP, CORS, Referrer-Policy, Permissions-Policy, X-Content-Type-Options
│ 4. Runtime / DOM Security │ Trusted Types, HTML Sanitizer API, Web Crypto, WebAuthn, cookie security
└─────────────────────────────────────────────┘
Never treat security features as isolated checkboxes — they compose. A tight CSP is weaker without HTTPS; Trusted Types is weaker without require-trusted-types-for.
2. Browser Security Model
Same-Origin Policy (SOP)
- An origin = scheme + host + port.
https://a.com ≠ http://a.com ≠ https://a.com:8080.
- Scripts can only read responses from the same origin by default.
- SOP is the foundation everything else protects.
Secure Contexts
- Many powerful APIs (Web Crypto, WebAuthn, Service Workers, Geolocation) require a secure context:
https:// or http://localhost.
- Always develop and deploy on HTTPS. Never relax this for convenience.
Cross-Origin Isolation
Required to use SharedArrayBuffer and high-resolution timers (Spectre mitigations). Achieved with:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
These headers together set window.crossOriginIsolated = true. Only add them when you need the APIs they unlock — they break some cross-origin resource loading.
Cross-Origin Resource Policy (CORP)
Prevents other origins from loading your resources (images, scripts, fonts) without your consent:
Cross-Origin-Resource-Policy: same-origin # most restrictive
Cross-Origin-Resource-Policy: same-site # allow same-site subdomains
Cross-Origin-Resource-Policy: cross-origin # allow all (use only for public CDN assets)
3. Transport Security
HTTPS / TLS
- Serve all content over HTTPS. No exceptions.
- Use TLS 1.2+ (TLS 1.3 preferred). Disable older protocol versions server-side.
- Redirect HTTP → HTTPS with a 301 at the server or load balancer, then enforce with HSTS.
HTTP Strict Transport Security (HSTS)
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
max-age: at minimum 1 year (31536000 seconds) for production; use 2 years (63072000) if preloading.
includeSubDomains: essential if any subdomain handles sensitive data.
preload: submit to hstspreload.org only after the above are stable.
- Do not set HSTS on HTTP responses or before redirects are working — it can lock users out.
Mixed Content
- Never load HTTP sub-resources from HTTPS pages — browsers block them or warn.
- Use the
upgrade-insecure-requests CSP directive during migrations to auto-upgrade requests.
- Use
block-all-mixed-content for strict enforcement (deprecated in favour of browser defaults, but still communicates intent).
Certificate Transparency
- All public CAs must submit certificates to CT logs (enforced by browsers).
- Monitor for unexpected certificates using services like crt.sh or Google's Transparency Report.
4. Content Security Policy (CSP)
CSP is the most powerful application-layer XSS defense. Understand the directive hierarchy before writing any policy.
Core Directives
| Directive | Controls |
|---|
default-src | Fallback for all fetch directives not explicitly set |
script-src | JavaScript sources |
style-src | CSS sources |
img-src | Image sources |
connect-src | fetch(), XHR, WebSocket destinations |
font-src | Web fonts |
frame-src | Allowed <iframe> sources |
frame-ancestors | Who may embed this page (replaces X-Frame-Options) |
object-src | <object>, <embed>, <applet> |
base-uri | Restricts <base href> (prevents base-tag injection) |
form-action | Where <form> may submit |
upgrade-insecure-requests | Auto-upgrades HTTP sub-resource requests to HTTPS |
require-trusted-types-for 'script' | Enforces Trusted Types on DOM sinks |
trusted-types | Allowlist of valid Trusted Types policy names |
report-to | Endpoint name for violation reports |
Writing a Strict CSP
Never use 'unsafe-inline' or 'unsafe-eval' in script-src — they defeat XSS protection entirely.
Preferred: nonce-based CSP (works for SSR apps)
Content-Security-Policy:
default-src 'none';
script-src 'nonce-{RANDOM}' 'strict-dynamic';
style-src 'nonce-{RANDOM}';
img-src 'self' data:;
connect-src 'self';
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
require-trusted-types-for 'script';
trusted-types my-policy;
report-to csp-endpoint;
- Generate a fresh, cryptographically random nonce per request (at least 128 bits, base64-encoded).
'strict-dynamic' propagates trust to scripts loaded by a trusted script — removes the need to allowlist every CDN.
'none' for object-src is non-negotiable; Flash and plugins are dead vectors.
Hash-based CSP (works for static inline scripts)
script-src 'sha256-{BASE64_HASH}';
Compute the SHA-256 hash of the exact script content (no trailing newline differences). Better for static sites; harder to maintain.
Report-Only Mode (use before enforcing)
Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint;
Violations are reported but not blocked. Always deploy in report-only first and monitor for at least a week before switching to enforcement.
Violation Reporting
Connect CSP to the Reporting API (see Section 7):
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Content-Security-Policy: ...; report-to csp-endpoint;
5. Other Critical HTTP Security Headers
Set all of these on every response unless there is a specific reason not to.
# Prevent MIME-type sniffing (required when serving user-uploaded files)
X-Content-Type-Options: nosniff
# Clickjacking protection (use CSP frame-ancestors for more control)
X-Frame-Options: DENY
# Referrer policy: send origin only on same-origin, nothing cross-origin
Referrer-Policy: strict-origin-when-cross-origin
# Permissions policy: disable what you don't use
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
# Cross-origin isolation (if needed for SharedArrayBuffer)
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Never emit X-Powered-By, Server, or other fingerprinting headers in production.
6. Trusted Types API
Trusted Types prevent DOM-based XSS by enforcing that data passes through a sanitisation function before reaching injection sinks.
Injection Sinks to Guard
| Type | Sinks |
|---|
| HTML sinks | element.innerHTML, element.outerHTML, element.insertAdjacentHTML(), document.write(), ShadowRoot.innerHTML, Range.createContextualFragment() |
| Script sinks | eval(), Function(), setTimeout(string), setInterval(string), HTMLScriptElement.text |
| URL sinks | HTMLScriptElement.src, Worker() constructor URL, ServiceWorkerContainer.register() |
Enabling Enforcement via CSP
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types my-policy;
With this set, passing a plain string to any HTML sink throws a TypeError.
Creating and Using a Policy
const policy = trustedTypes.createPolicy("my-policy", {
createHTML: (input) => {
const el = document.createElement("div");
el.setHTML(input);
return el.innerHTML;
},
createScriptURL: (url) => {
const allowed = ["https://cdn.example.com/"];
if (allowed.some((prefix) => url.startsWith(prefix))) return url;
throw new Error(`Disallowed script URL: ${url}`);
},
createScript: (script) => {
throw new Error("Dynamic script evaluation is not allowed");
},
});
const trustedHTML = policy.createHTML(userInput);
element.innerHTML = trustedHTML;
element.innerHTML = userInput;
Tinyfill for Non-Supporting Browsers
if (typeof trustedTypes === "undefined") {
trustedTypes = { createPolicy: (name, rules) => rules };
}
The transformation functions still run in non-supporting browsers — same code path, same protection, no enforcement overhead.
Default Policy for Migration
During migration from legacy code, create a "default" policy that logs violations while still sanitizing:
trustedTypes.createPolicy("default", {
createHTML(value) {
console.warn(
"Unsafe innerHTML assignment detected — refactor this!",
new Error().stack,
);
return DOMPurify.sanitize(value);
},
});
This intercepts all raw string sink assignments and routes them through sanitization while you refactor.
7. HTML Sanitizer API
The native Sanitizer / setHTML() API (Baseline 2026, not yet fully cross-browser — use DOMPurify as fallback) provides browser-native HTML sanitization.
Safe Methods (XSS-safe by default)
element.setHTML(untrustedString);
element.setHTML(untrustedString, { sanitizer });
Document.parseHTML(untrustedString);
These methods always enforce the XSS-safe baseline: removes <script>, <iframe>, <object>, <embed>, <frame>, <use>, and all event handler attributes (onclick, etc.).
Custom Allow-List Sanitizer
const sanitizer = new Sanitizer({
elements: ["p", "b", "i", "em", "strong", "a", "ul", "ol", "li", "br"],
attributes: ["href", "title"],
});
element.setHTML(untrustedString, { sanitizer });
Custom Remove-List Sanitizer
const sanitizer = new Sanitizer({ removeElements: ["table", "form"] });
element.setHTML(untrustedString, { sanitizer });
Integration with Trusted Types
const policy = trustedTypes.createPolicy("sanitizer-policy", {
createHTML: (input) => {
const div = document.createElement("div");
div.setHTML(input);
return div.innerHTML;
},
});
element.innerHTML = policy.createHTML(untrustedInput);
Cross-Browser Strategy
Until the Sanitizer API has full cross-browser support, use:
function safeSetHTML(element, untrustedString, sanitizer = null) {
if (typeof element.setHTML === "function") {
element.setHTML(untrustedString, sanitizer ? { sanitizer } : undefined);
} else {
element.innerHTML = DOMPurify.sanitize(untrustedString);
}
}
8. Reporting API
The Reporting API provides a consistent mechanism to receive security violation reports from browsers, without relying on in-page JavaScript (which can crash with the page).
Server-Side Endpoints
# Define named endpoints
Reporting-Endpoints: csp-endpoint="https://reports.example.com/csp", default="https://reports.example.com/default"
# Direct CSP violations to the named endpoint
Content-Security-Policy: ...; report-to csp-endpoint;
# COEP violations
Cross-Origin-Embedder-Policy: require-corp; report-to default;
Reports are delivered as POST requests with Content-Type: application/reports+json. Your endpoint must handle JSON arrays of report objects.
Report Types
| Report type | Source |
|---|
csp-violation | CSP enforcement violations |
coep | Cross-Origin-Embedder-Policy violations |
deprecation | Use of deprecated browser APIs |
intervention | Browser interventions (e.g., blocked autoplay) |
integrity-violation | Subresource Integrity failures |
In-Page ReportingObserver
Use ReportingObserver for runtime monitoring and developer tooling:
const observer = new ReportingObserver(
(reports, observer) => {
reports.forEach((report) => {
console.warn(`[${report.type}]`, report.body);
fetch("/internal/report", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(report),
});
});
},
{
types: ["csp-violation", "deprecation", "intervention"],
buffered: true,
},
);
observer.observe();
Important: ReportingObserver is less reliable than server endpoints for security violations — a page crash stops it. Use both: server endpoints for production security monitoring, observers for dev-mode diagnostics.
9. Cookie Security
Every Set-Cookie on a sensitive session or auth token must include all of:
Set-Cookie: sessionId=abc123;
Secure; # HTTPS only — non-negotiable
HttpOnly; # JavaScript cannot read it — prevents XSS token theft
SameSite=Lax; # Sent on top-level navigations; blocks CSRF on most forms
Path=/;
Max-Age=3600;
__Host- prefix # Forces Secure + Path=/ + no Domain attribute (strongest)
SameSite Values
| Value | Cross-site GET | Cross-site POST | Same-site |
|---|
Strict | ❌ | ❌ | ✅ |
Lax (default) | ✅ (top-level nav) | ❌ | ✅ |
None | ✅ | ✅ | ✅ (requires Secure) |
- Use
SameSite=Strict for admin/sensitive cookies.
- Use
SameSite=Lax for session cookies where you need cross-site link navigation to work.
- Use
SameSite=None; Secure only for intentional third-party contexts (payment iframes, OAuth callbacks, embeds).
Cookie Prefixes
__Host-name: Forces Secure, Path=/, and no Domain attribute. Strongest binding.
__Secure-name: Forces Secure only.
Partitioned Cookies (CHIPS)
For third-party cookies that must survive privacy changes:
Set-Cookie: tracking=abc; Secure; SameSite=None; Partitioned;
Partitioned cookies are isolated per top-level site, preventing cross-site tracking.
10. Authentication Security
WebAuthn / Passkeys
Prefer WebAuthn over passwords where possible — it is phishing-resistant, bound to the origin, and requires a secure context.
const credential = await navigator.credentials.create({
publicKey: {
challenge: cryptoRandomBytes,
rp: { name: "My App", id: "example.com" },
user: { id: userId, name: userEmail, displayName: userName },
pubKeyCredParams: [
{ type: "public-key", alg: -7 },
{ type: "public-key", alg: -257 },
],
authenticatorSelection: {
residentKey: "required",
userVerification: "required",
},
timeout: 60000,
attestation: "none",
},
});
Always verify the WebAuthn response server-side: origin, rpIdHash, clientDataHash, signature, and counter.
Credential Management API
await navigator.credentials.store(new PasswordCredential({ id, password }));
const cred = await navigator.credentials.get({
password: true,
mediation: "optional",
});
11. Web Crypto API
Use crypto.subtle for all client-side cryptographic operations. It is only available in secure contexts.
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"],
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
plaintext,
);
const plaintext = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
ciphertext,
);
const baseKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveKey"],
);
const derivedKey = await crypto.subtle.deriveBits(
{ name: "PBKDF2", salt, iterations: 600000, hash: "SHA-256" },
baseKey,
256,
);
Never use Math.random() for anything security-related. Always use crypto.getRandomValues().
12. CORS Configuration
CORS is a server-side permission system, not a security boundary. Misconfiguring it lets other origins read your data.
Response Headers
Access-Control-Allow-Origin: https://trusted.example.com # NEVER use '*' for credentialed requests
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true # only when needed + origin is explicit
Access-Control-Max-Age: 86400 # cache preflight for 1 day
Common CORS Mistakes
const origin = req.headers.origin;
res.setHeader("Access-Control-Allow-Origin", origin);
const ALLOWED_ORIGINS = new Set([
"https://app.example.com",
"https://admin.example.com",
]);
if (ALLOWED_ORIGINS.has(req.headers.origin)) {
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
res.setHeader("Vary", "Origin");
}
Always set Vary: Origin when the Access-Control-Allow-Origin value varies — without it, caches may serve the wrong origin's CORS response.
13. Subresource Integrity (SRI)
For any third-party script or stylesheet, add an integrity attribute:
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-{BASE64_HASH}"
crossorigin="anonymous"
></script>
Generate hashes:
openssl dgst -sha384 -binary lib.js | openssl base64 -A
Or use shasum -a 384 lib.js | awk '{print $1}' | xxd -r -p | base64.
Pair with CSP's require-sri-for (experimental) or rely on the integrity attribute enforcement already built into browsers. For full enforcement, combine with an Integrity-Policy header and the Reporting API.
14. Attack Patterns & Defenses Reference
XSS (Cross-Site Scripting)
- Root cause: Untrusted data reaching DOM sinks or reflected in HTML without encoding.
- Defenses: Strict CSP (nonce-based) + Trusted Types + HTML Sanitizer API. Output-encode all user data in templates (context-aware: HTML, attribute, JS, URL, CSS escaping).
- Key rule: Never pass user input to
innerHTML, eval(), document.write(), or setTimeout(string) without going through a Trusted Types policy.
CSRF (Cross-Site Request Forgery)
- Root cause: Browser automatically includes cookies on cross-origin requests.
- Defenses:
SameSite=Lax/Strict cookies + server-side CSRF tokens for state-changing endpoints. Check Origin / Referer headers server-side as a secondary control.
Clickjacking
- Root cause: Attacker embeds your page in an invisible
<iframe> and tricks users into clicking.
- Defenses:
Content-Security-Policy: frame-ancestors 'none' (or 'self'). Legacy: X-Frame-Options: DENY.
MIME Sniffing
- Root cause: Browser guesses content type and executes a text file as JS.
- Defense:
X-Content-Type-Options: nosniff on every response. Set correct Content-Type for all served files.
Open Redirect
- Root cause: Server or client redirects to an attacker-controlled URL from a parameter.
- Defense: Validate redirect targets against an allowlist of paths. Never redirect to a full URL from user input.
Prototype Pollution
- Root cause: Attacker writes to
__proto__ or Object.prototype via object merge / spread patterns.
- Defense: Use
Object.create(null) for data dictionaries. Freeze Object.prototype in sensitive contexts. Validate and sanitize any data used in object merge operations.
15. Security Headers Audit Checklist
When reviewing or generating server configuration, verify every response includes:
✅ Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
✅ Content-Security-Policy: [strict nonce/hash policy]; report-to csp-endpoint
✅ X-Content-Type-Options: nosniff
✅ X-Frame-Options: DENY (or use CSP frame-ancestors)
✅ Referrer-Policy: strict-origin-when-cross-origin
✅ Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
✅ Cross-Origin-Resource-Policy: same-origin (or appropriate value)
✅ Reporting-Endpoints: [endpoint definitions]
❌ Remove: X-Powered-By, Server, X-AspNet-Version, X-AspNetMvc-Version
Use MDN's HTTP Observatory (https://developer.mozilla.org/en-US/observatory) or securityheaders.com to grade any production site.
16. Code Patterns: Do and Don't
element.innerHTML = userInput;
element.innerHTML = `<span>${data}</span>`;
element.setHTML(userInput);
element.innerHTML = policy.createHTML(userInput);
const span = document.createElement("span");
span.textContent = userInput;
element.appendChild(span);
eval(userCode);
setTimeout(userString, 1000);
new Function(userString)();
const fn = policy.createScript(sanitizedScript);
location.href = userInput;
element.src = userInput;
function safeURL(input) {
try {
const url = new URL(input, location.origin);
if (!["https:", "http:"].includes(url.protocol))
throw new Error("Bad scheme");
return url.href;
} catch {
return null;
}
}
localStorage.setItem("token", jwt);
const token = Math.random().toString(36);
const token = crypto.randomUUID();
const bytes = crypto.getRandomValues(new Uint8Array(32));
17. Implementation Order for a New Application
Follow this sequence when hardening a new or existing web application:
- HTTPS everywhere — enforce at infrastructure level; add HSTS header.
- Secure cookies — add
Secure; HttpOnly; SameSite=Lax to all session cookies; use __Host- prefix.
- Strict CSP in report-only — deploy
Content-Security-Policy-Report-Only with a nonce-based policy; set up a reporting endpoint; monitor for 1–2 weeks.
- Other security headers —
X-Content-Type-Options, Referrer-Policy, Permissions-Policy, X-Frame-Options.
- Enforce CSP — switch from Report-Only to enforcement once violations are resolved.
- Add Trusted Types — add
require-trusted-types-for 'script' to CSP; create policies; use default policy with logging during migration.
- Adopt native Sanitizer API (with DOMPurify fallback) — replace all
innerHTML assignments with setHTML() or Trusted Types–wrapped sanitization.
- SRI for third-party resources — add
integrity hashes to all <script> and <link> tags loading from CDNs.
- Reporting API — configure
Reporting-Endpoints; connect to a log aggregator; alert on sustained CSP violations.
- WebAuthn — replace password-only auth with passkeys or MFA; use Credential Management API.