Frontend Labs
⋯

Chapter 43 — Cross-site scripting (XSS)

In one sentence

XSS is injection into a web page: the attacker gets their JavaScript to run in your users' browsers in your site's origin, and from there can do anything the user can.

The three types

   STORED (persistent)   payload saved server-side (a comment, profile,
                         product review) and served to every viewer.
                         Worst impact -- can self-propagate (Samy worm).

   REFLECTED             payload in the request (query param, path) echoed
                         straight into the response. Needs a lure (a crafted
                         link). One victim per click.

   DOM-BASED            no server involvement: client-side JS reads
                         attacker-controlled input (location.hash,
                         postMessage, document.referrer) and writes it into
                         a dangerous sink (innerHTML, eval, document.write).

Why it's dangerous

Once script runs in your origin it can:

   - read the DOM (everything on the page the user sees)
   - make authenticated requests as the user (SOP allows same-origin;
     cookies attach automatically)
   - read non-HttpOnly cookies, localStorage, sessionStorage
   - keylog, capture form input, phish with a fake login overlay
   - pivot: hit internal APIs the user's browser can reach
   - persist (service worker) and self-spread (stored XSS)

HttpOnly stops cookie theft; it does not stop any of the rest.

The fix: context-aware output encoding + CSP

   ROOT CAUSE: putting untrusted data into HTML without encoding it for
   THAT context.

   1. CONTEXT-AWARE OUTPUT ENCODING (the primary fix)
        HTML body:        & < > -> &amp; &lt; &gt;
        HTML attribute:   also encode quotes; always quote attributes
        JavaScript:       don't. Serialise as JSON into a data attribute
                          or <script type="application/json">, then read it.
        URL:              percent-encode; validate the scheme (no javascript:)
        CSS:              avoid entirely; strict allowlist
      Modern frameworks (React, Angular, Vue, Svelte) auto-encode by
      default. XSS then comes from the ESCAPE HATCHES:
        React    dangerouslySetInnerHTML
        Angular  bypassSecurityTrust*
        Vue      v-html
        any      .innerHTML, document.write, jQuery .html(), eval,
                 new Function, setTimeout("string")

   2. FOR RICH HTML INPUT (user must submit formatted text):
        sanitise with a vetted library -- DOMPurify -- with a strict
        allowlist. Never a homemade regex "sanitiser".

   3. CONTENT SECURITY POLICY (defence in depth -- assume 1 & 2 will
      sometimes fail)
        Content-Security-Policy:
          default-src 'self';
          script-src 'self' 'nonce-<random-per-response>';
          object-src 'none'; base-uri 'none'; frame-ancestors 'none'
      - NO 'unsafe-inline', NO 'unsafe-eval', NO broad host allowlists
        (they're bypassable via JSONP/old Angular on those hosts).
      - Prefer a NONCE- or HASH-based, or STRICT-DYNAMIC policy.
      - Add a report-uri/report-to endpoint and watch it.

   4. Trusted Types (Chromium): make the DOM XSS sinks refuse plain
      strings -> forces all sink writes through a vetted policy.
        Content-Security-Policy: require-trusted-types-for 'script'

Practice (40 min) — lab only

   1. Juice Shop / PortSwigger:
      - Reflected XSS: find a parameter echoed into the page. Pop
        alert(document.domain). Then escalate to reading a cookie or
        making an authenticated request in the console.
      - Stored XSS: get a payload into a field that's rendered to other
        users (a review, a name). Confirm it fires on page load.
      - DOM XSS: find JS that writes location.hash into innerHTML.
        Craft the URL.
   2. Fix locally:
      - 15-line app that reflects ?name= into HTML via string concat.
        Confirm <script>alert(1)</script> runs.
      - Add context-aware encoding (or use a templating engine with
        autoescape). Confirm the payload now renders as visible text.
      - Add a strict nonce-based CSP. In devtools, watch an inline
        <script> without the nonce get BLOCKED, and see the violation
        report.
   3. Add a "bio" field that allows <b>/<i>. Implement it with DOMPurify
      (allowlist b,i,em,strong only). Try to sneak <img onerror> past it.
   4. Score your headers: run the fixed app through securityheaders.com
      (local via ngrok, or read the curl -I output against the checklist).

Common confusions

  • "I escape < and > so I'm safe." Only for HTML body context. In an attribute without quotes, or in a javascript:/href, or inside a <script>, or in CSS — different rules. Encode for the context, or don't put data there.
  • "CSP fixes XSS." CSP mitigates it. unsafe-inline or a sloppy host allowlist neuters it. Encoding is still the fix; CSP is the seatbelt.
  • "My framework auto-escapes, so no XSS." True until someone uses dangerouslySetInnerHTML / v-html / bypassSecurityTrust, or builds a javascript: URL, or does client-side templating of user data.
  • "HttpOnly cookies make XSS low severity." No. XSS = full control of the session in-browser regardless of cookie theft.
  • "A regex strips <script>." Trivially bypassed (<img onerror>, <svg onload>, mixed case, nested, null bytes). Use DOMPurify.

Check yourself

  1. Name the three XSS types and the key difference of each.
  2. With HttpOnly session cookies, what can an XSS still do? (List four.)
  3. What is "context-aware output encoding" and why isn't one encoding enough?
  4. What makes a CSP weak? Name two things that must NOT be in script-src.
  5. How should you handle a field where users legitimately submit some HTML?

Further reading

  • Cheat sheets: OWASP "Cross Site Scripting Prevention", "DOM based XSS Prevention", "Content Security Policy".
  • Tool: DOMPurify (github.com/cure53/DOMPurify) — read the README's threat model.
  • Course: PortSwigger Academy — XSS track (huge, excellent) and the "Content Security Policy" material.
  • Talk: "CSP: A Successful Mess Between Hardening and Mitigation" (Lukas Weichselbaum, Google) — why strict/nonce CSP beats allowlists.
  • Site: Google's csp-evaluator.withgoogle.com — paste your policy, see the bypasses.

Generated from real-life-guide.md · 8 Oct 2026 16:36