Prevent XSS Attacks: Escaping, CSP and Trusted Types
Prevent XSS Attacks: Escaping, CSP and Trusted Types
Cross-Site Scripting (XSS) lets an attacker run arbitrary JavaScript in the context of your domain. Consequences: session-cookie theft, keylogging, phishing, full account takeover. XSS sits under OWASP Top 10 Injection and is still a fixture of every pentest report. This post covers the three flavours of XSS, context-aware escaping, CSP, and Trusted Types as defence-in-depth.
The three flavours of XSS
- Stored XSS — payload is persisted (comment, profile) and served to every visitor. Worst case.
- Reflected XSS — payload sits in a URL parameter and is echoed back in the response. Requires a phishing click.
- DOM-based XSS — payload handled by client-side JS (
innerHTML,document.write), never touching the server.
Context-aware escaping
Core rule: the escape function depends on the output context.
<!-- HTML body -->
<div>{{ escaped_html }}</div> ← &, <, >, ", '
<!-- HTML attribute -->
<img alt="{{ escaped_attr }}"> ← same + quotes
<!-- JS string literal -->
var x = "{{ escaped_js }}"; ← escape \, ', ", newline, unicode
<!-- URL attribute -->
<a href="{{ url_encoded }}"> ← rawurlencode + scheme allowlist
<!-- CSS value -->
color: {{ css_escaped }}; ← only [a-zA-Z0-9#%]
PHP
function e(string $s): string {
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
echo '<div>', e($userInput), '</div>';
JavaScript (DOM)
// Dangerous
element.innerHTML = userInput;
// Safe
element.textContent = userInput;
// When you really need HTML — use DOMPurify
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);
React / Vue
React escapes {variable} by default. The only traps: dangerouslySetInnerHTML and dynamic href:
// Bad
<a href={userInput}> ... // javascript: scheme
// Good
const safeHref = userInput.startsWith('http') ? userInput : '#';
<a href={safeHref}>
CSP as the last line
Even perfect escaping cannot save you from a framework zero-day. CSP turns a successful XSS into a blocked script. Minimum:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-RANDOM' 'strict-dynamic'; object-src 'none'; base-uri 'self'
Details in CSP setup.
Trusted Types (Chrome 83+)
Trusted Types force every DOM sink (innerHTML, script.src) to only accept strings that went through a policy function — dramatically reducing DOM XSS surface:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default
trustedTypes.createPolicy('default', {
createHTML: (s) => DOMPurify.sanitize(s),
});
element.innerHTML = 'raw html'; // TypeError without policy
HttpOnly cookies
The HttpOnly flag prevents JS from reading cookies — so even a successful XSS cannot exfiltrate the session. Always combine with Secure and SameSite. See Cookie Security.
Server-side checks
- WAF (mod_security, Cloudflare) catches basic <script> payloads
- Whitelist-based input validation (email, URL, integer)
- Content-Type: application/json on API документацию responses — never text/html for data
- HTML sanitisation for rich editors: HTMLPurifier (PHP), DOMPurify (JS)
Testing
Canonical test payload: <img src=x onerror="alert(1)">. If an alert fires, you have XSS. Static analysis: Semgrep, Snyk Code, SonarQube. Header hygiene via the Security Scanner.
FAQ
Is PHP's strip_tags enough? No — it's bypassed with <svg>, onerror, HTML entities. Use htmlspecialchars for body and DOMPurify/HTMLPurifier for rich content.
Does SSL/TLS проверку protect against XSS? No — XSS runs inside JS on the legitimate origin.
Is jQuery .html() dangerous? Yes — it's a wrapper over innerHTML. Use .text().
What about Markdown input? Render to sanitised HTML via marked + DOMPurify; never allow raw HTML passthrough.
Conclusion
Layered defence: context escaping + CSP + Trusted Types + HttpOnly cookies + WAF. Watch CSP reports through enterno monitors and validate headers with the Security Scanner. Related reads: CSP Setup, Cookie Security.
Check your website right now
Check now →