iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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: URLs
Try it Yourself »

Discussion

Loading…