Cheatsheet
A one-page reference of XSS sinks, the data they accept, and the right encoding for each. Print it, paste it into a code-review checklist, and stop guessing whether htmlspecialchars or encodeURIComponent is right for the spot you are looking at.
Sinks, contexts, encodings, defaults
EXAMPLE
<!-- ===== 1. The four output contexts that matter ===== -->
<!-- Each needs a DIFFERENT escaping function. Wrong function = bug. -->
<!-- Context A: HTML body / text content -->
<!-- Use: htmlspecialchars (PHP), {{ }} (Blade/Twig/Mustache), textContent (DOM) -->
<p>Hello, <?= htmlspecialchars($name, ENT_QUOTES | ENT_HTML5, 'UTF-8') ?></p>
<!-- Context B: HTML attribute value -->
<!-- Use: htmlspecialchars WITH the quote around the attribute -->
<input value="<?= htmlspecialchars($value, ENT_QUOTES, 'UTF-8') ?>">
<!-- Context C: JavaScript string literal -->
<!-- Use: json_encode (PHP) / JSON.stringify with safe flags -->
<script>
const user = <?= json_encode($user, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
</script>
<!-- Context D: URL parameter -->
<!-- Use: urlencode / rawurlencode / encodeURIComponent -->
<a href='/search?q=<?= urlencode($q) ?>'>search</a>
// JS: const u = '/search?q=' + encodeURIComponent(q);
<!-- ===== 2. Dangerous sinks to grep for ===== -->
<!-- PHP / Blade: echo \$_GET, echo \$_POST, {!! \$x !!}, document.write -->
<!-- JS: .innerHTML, .outerHTML, .insertAdjacentHTML,
eval, setTimeout(string), Function(string),
document.write, location.href = userInput -->
<!-- React: dangerouslySetInnerHTML -->
<!-- Vue: v-html -->
<!-- Angular: bypassSecurityTrust* -->
<!-- ===== 3. Defence-in-depth headers ===== -->
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; require-trusted-types-for 'script';
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
<!-- ===== 4. Framework safe defaults ===== -->
<!-- Blade / React / Vue / Angular default to ESCAPING. -->
<!-- The bug is almost always the ESCAPE HATCH:
{!! !!}, dangerouslySetInnerHTML, v-html, bypassSecurityTrust*. -->
<!-- Treat every use of those as a code review red flag with rationale. -->
<!-- ===== 5. Sanitise, do not regex ===== -->
<!-- If you must allow user HTML (rich text comments), sanitise with
DOMPurify (JS) or HTMLPurifier (PHP). Never with a hand-rolled regex. -->
<!-- <textarea> -> DOMPurify.sanitize(input, { USE_PROFILES: { html: true } }) -->
<!-- ===== 6. URL allow-list — protocol matters ===== -->
function safeUrl(s) {
try {
const u = new URL(s, location.origin);
return ['http:', 'https:', 'mailto:'].includes(u.protocol) ? u.toString() : '#';
} catch { return '#'; }
}
Why it matters
Pick the right escaping function for the context — not the most familiar one. A value escaped for HTML and dropped into a JavaScript string is still XSS-vulnerable. Tag the context in the variable name (\$value_html, \$value_js, \$value_url) and the right encoder becomes obvious at the call site.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Encode for the context | Escape by default in templates | DOMPurify for rich text // CSP + Trusted Types | HttpOnly + SameSite cookies | Avoid javascript: URLsTry it Yourself »
Discussion
Loading…