HTML entity encoding and Content Security Policy are both trying to prevent Cross-Site Scripting — but they operate at completely different layers, and using only one while skipping the other leaves a vulnerability that attackers know exactly how to exploit
The previous articles on this site covered HTML entity basics, XSS and the five encoding contexts, Unicode and UTF-8, double encoding dangers, and which entities are still necessary in UTF-8. This article addresses CSP (Content Security Policy) as the second layer of XSS defence — what it is, how it interacts with HTML encoding, and why modern security requires both working together.
What HTML encoding alone cannot prevent
HTML entity encoding — converting < to <, " to ", etc. — prevents injected characters from being interpreted as HTML structure. It's the primary defence against reflected and stored XSS. But it has limits:
Limit 1: Injections into JavaScript context. If user input is placed inside a <script> block, HTML encoding doesn't help — JavaScript has its own parsing rules. The browser parses the script block as JavaScript, not HTML, before HTML encoding would apply:
<script>
var userName = "<?= htmlspecialchars($userInput) ?>";
// If $userInput is: "; alert(1); //
// After htmlspecialchars: "; alert(1); //
// htmlspecialchars doesn't encode ; or ( or ) — these are valid in HTML
// But in JavaScript context, this still executes alert(1)
</script>
Limit 2: JavaScript event handler context. onclick="doSomething('<?= htmlspecialchars($input) ?>')" — the input is inside a JavaScript string inside an HTML attribute. The escaping requirements differ from pure HTML context.
Limit 3: Third-party scripts. If an attacker compromises a CDN or third-party script your site uses, HTML encoding of your own content doesn't help — the injected script bypasses HTML encoding entirely by being loaded from an external source.
Content Security Policy addresses specifically these gaps — particularly the third-party script and inline script problems.
What Content Security Policy is
CSP is an HTTP response header (or <meta http-equiv> tag) that tells browsers which sources of content are legitimate for the page:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.trusted.com;
style-src 'self' 'unsafe-inline';
img-src *;
object-src 'none';
What this tells the browser:
default-src 'self': by default, only load content from the same originscript-src 'self' https://cdn.trusted.com: JavaScript can only come from this domain or the trusted CDNstyle-src 'self' 'unsafe-inline': CSS can come from this domain or inline (note:unsafe-inlineis a weakness)img-src *: images can come from anywhereobject-src 'none': no plugins (Flash, Java applets) at all
The XSS mitigation: even if an attacker injects a <script src="https://attacker.com/evil.js"> into your page, the browser checks the CSP and refuses to load the script — it's not from 'self' or https://cdn.trusted.com.
CSP nonces: the solution to inline scripts
The biggest practical challenge with CSP is that script-src 'unsafe-inline' is frequently needed for legitimate inline scripts — but unsafe-inline also allows injected inline scripts. This seemed like an unavoidable trade-off until nonces were introduced.
How nonces work:
- The server generates a cryptographically random nonce (once-use value) for each page request
- The nonce is included in the CSP header and in each legitimate inline script tag
- The browser only executes inline scripts that have the matching nonce
Content-Security-Policy: script-src 'nonce-2726c7f26c'
<script nonce="2726c7f26c">
// This legitimate script executes
doSomething();
</script>
<!-- An injected script without the nonce is blocked: -->
<script>alert("XSS")</script>
The attacker's problem: the nonce changes with every page request and is generated server-side. Even if an attacker sees the nonce in the current page's source, they can't use it in a future attack because the nonce will be different for the next request.
CSP hashes: the alternative to nonces for static content
For inline scripts that never change, CSP hashes provide an alternative to nonces:
Content-Security-Policy: script-src 'sha256-abc123...'
The hash is a SHA-256 digest of the exact script content. The browser computes the hash of each inline script and compares it to the allowed hashes in the CSP.
When hashes work well: static <script> blocks (e.g., analytics snippets with fixed content). When hashes fail: any script that includes dynamic content (even a timestamp or user ID inserted by the server) — the hash changes with the content.
The typical recommendation: use nonces for server-rendered pages (dynamic content), hashes for static content.
report-uri and report-to: CSP violation reporting
CSP includes a reporting mechanism that sends violation reports to a specified endpoint when the browser blocks something under the policy:
Content-Security-Policy:
default-src 'self';
script-src 'self';
report-uri /csp-violation-report
Or using the newer report-to directive with a report group:
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://your-site.com/csp-reports"}]}
Content-Security-Policy: default-src 'self'; report-to csp-endpoint
What violation reports reveal:
- Attempted XSS injections (attackers probing your site show up in violation reports)
- Legitimate content blocked by an over-restrictive CSP (browser extensions, legitimate third parties)
- Misconfigured CSP that's inadvertently blocking something your site needs
Report-Only mode (Content-Security-Policy-Report-Only: header) sends violation reports without actually blocking anything — the correct way to test a new CSP before enforcing it.
The limitations of HTML entities for email content
When rendering HTML email (rather than web pages), entity encoding has a different threat model:
Email clients render HTML but typically don't execute JavaScript (most clients block scripts entirely). The XSS risk is lower, but HTML injection (injecting <a href="mailto:attacker@...">) and phishing via styled content remains relevant.
Entities still matter in email HTML for displaying characters that have HTML significance — but the security context differs from web page rendering. The sanitisation requirements for email HTML are about preventing malicious link injection and visual spoofing rather than script execution.
How to use the HTML Entities tool on sadiqbd.com
- For output encoding review: paste user-generated content to see which characters would be entities in HTML context — verifying that your application is encoding the full set of relevant characters
- For CSP nonce preparation: the tool handles encoding of the nonce value itself if it needs to appear inside an HTML attribute context — though nonces are typically injected server-side, not via this tool
- For double-encoding diagnosis: paste already-encoded content to see if further encoding is being applied — double-encoding (producing
&lt;when you intended<) is a common template rendering bug
Frequently Asked Questions
If I implement a strict CSP with nonces, do I still need to HTML-encode user input?
Yes, absolutely — CSP and HTML encoding are complementary defences, not alternatives. CSP prevents injected scripts from loading or executing. HTML encoding prevents injected markup from being interpreted as HTML structure. Without HTML encoding, even with a strict CSP: (1) visual HTML injection remains possible (injecting <h1>Fake Login Form</h1> is blocked by CSP if it contains scripts, but visual injection still works), (2) CSS injection via <style> tags can cause visual spoofing, and (3) certain reflected attacks don't require script execution (link injection, form action hijacking). Defence in depth — both layers — is the correct approach.
Is the HTML Entities tool free? Yes — completely free, no sign-up required.
Try the HTML Entities tool free at sadiqbd.com — encode and decode HTML entities for any context instantly.