Handbooks / Web Security (OWASP) / Chapter 2

Cross-Site Scripting (XSS)

7 pages · ~5 min✓ Reviewed

Builds on Injection. Next up: Authentication and Sessions.

Web Security (OWASP)Page 1

XSS Mental Model: Stored, Reflected, DOM

1. What XSS Is

  • Attacker data runs as code in the victim's browser
  • Runs with the page's origin: reads DOM, sends requests, acts as the user

2. Three Types

StoredReflectedDOM
Payloadsaved (comment, profile name)in request (query, form field)client source to sink
Needs a click?noyes, crafted URLyes
Server sees ityesyesoften never (#fragment)
Impactevery viewerone victimone victim

3. DOM: Source to Sink

  • Sources: location.hash, location.search, document.referrer, postMessage
  • Sinks: innerHTML, document.write, eval, setTimeout(string), element.href
  • location.hash, postMessage skip the server; location.search is sent
javascript
// #<img src=x onerror=alert(1)>
el.innerHTML = decodeURIComponent(location.hash.slice(1))
el.textContent = decodeURIComponent(location.hash.slice(1))

innerHTML runs the alert; textContent is safe

4. Interview Point

  • Never reaches the server? DOM XSS via #fragment; grep for sources and sinks
  • Stored worse? No crafted link; hits every viewer
  • Sources, sinks? location.hash, postMessage; innerHTML, eval
Web Security (OWASP)Page 2

Context-Aware Output Encoding

1. The Core Rule

  • Encode on output, for the exact context where the data lands
  • Input validation is defence in depth, not the fix
  • Each context has its own encoder

2. One Encoder per Context

  1. Untrusted data
  2. Find the context
  3. Apply its encoder
  4. Output
ContextEncoderResult
HTML body< > & " ' to entities&lt; &gt; &amp; &quot; &#x27;
HTML attributequote value, encode quotesvalue="...&quot;..."
JS string\xHH or JSON.stringify in a data attribute\x3c
URL parameterencodeURIComponenta%26b
URL schemeallowlist http / httpsnever javascript:
CSSits own CSS escaper\3c

3. Common Mistakes

  • Unquoted attribute: a space breaks out, x onmouseover=alert(1)
  • HTML entities do not stop a javascript: href
  • HTML-escaping a value placed in <script> is the wrong encoder
Right encoder, right context

Quote every attribute and allowlist http/https for URLs.

Web Security (OWASP)Page 3

CSP and HttpOnly Cookies as Containment

1. CSP: Second Layer

  • Response header listing allowed script sources
  • Limits damage when an escaping bug slips through
  • Not a replacement for output encoding
http
Content-Security-Policy: default-src 'self';
  script-src 'nonce-R4nd0m' 'strict-dynamic';
  object-src 'none'; base-uri 'none'

nonce: fresh per response, unguessable

In script-srcResult
'unsafe-inline' (no nonce/hash)injected inline script runs
'unsafe-eval'eval() allowed
https:, *.cdn.comany script on those hosts runs
  • Inline event handlers are blocked unless allowed
  • Roll out with Content-Security-Policy-Report-Only + report-to

2. HttpOnly Cookies

  • HttpOnly hides the session cookie from document.cookie
  • Add Secure and SameSite=Lax or Strict
  • Does NOT stop XSS sending authenticated requests as the user

3. Interview Point

  • HttpOnly? Stops cookie theft, not in-page requests as the user
  • Nonce vs allowlist? Allowed hosts can serve usable scripts; a nonce trusts only your tags
  • Deploy safely? Report-Only, fix reports, then enforce
Web Security (OWASP)Page 4

Framework Auto-Escaping and Its Escape Hatches

1. Defaults and hatches

  • Escaped by default: JSX, Angular, Vue {{ }}, Django, Rails
  • Jinja2 only with autoescape on (Flask does for .html)
Hatch (escaping off)Safe
ReactdangerouslySetInnerHTML{text}
Vuev-html{{ }}
Angular[innerHTML] + bypassSecurityTrustHtml{{ }}
Jinja2|safe, autoescape offdefault
Djangomark_safe{{ }}
Railsraw, html_safe<%= %>

2. Raw HTML? Sanitise

  • Allowlist sanitiser: DOMPurify (client) or a server-side equivalent
  • Run it immediately before rendering
  • Never write your own regex filter
javascript
el.innerHTML = DOMPurify.sanitize(dirty)

3. Still unprotected

  • href/src with javascript: URLs
  • style and event-handler attributes
  • Server-side template injection
  • Direct DOM writes: ref.innerHTML
Text positions only

Allowlist http/https schemes for user URLs.

  • Three hatches, fixes? v-html, |safe, mark_safe: use default {{ }} escaping
  • Why is <a href={userUrl}> risky? React 18 and earlier run javascript: on click; React 19 blocks it, still allowlist
  • Sanitiser instead of encoding? When users must supply real HTML
Web Security (OWASP)Page 5

Common XSS Mistakes

1. Filters and encoders

MistakeBypassFix
Blacklist <script><img src=x onerror=…>, <svg onload=…>, case, nested tagsEncode + allowlist
Wrong contextHTML-escaped value in a JS string, unquoted attribute or URLEncoder per exact context

2. Sinks and containment

  • innerHTML, document.write parse markup
  • textContent, innerText never do
javascript
el.innerHTML = userText;   // parses markup
el.textContent = userText; // plain text

one-line fix

  • HttpOnly alone: XSS still sends requests as the user
  • CSP alone: it is only a second layer
  • 'unsafe-inline' with no nonce or hash: injected inline script runs
Useless CSP

Without a nonce or hash, unsafe-inline lets injected inline script run, so CSP stops almost no XSS.

3. Interview Point

  • Why does a blacklist fail? Too many tags, events and case tricks; allowlist instead.
  • Fix for innerHTML with user text? Use textContent.
  • Which mistake makes CSP useless? 'unsafe-inline' without a nonce or hash.
Web Security (OWASP)Page 6

XSS Defence Cheat Sheet

1. Three types

TypePayloadServer sees
Storedsaved, hits all viewersyes
Reflectedin the request, needs a linkyes
DOMclient source to sink, e.g. #fragmentoften never

2. Encode by context

ContextEncoding
HTML bodyentities &lt; &gt; &amp;
Attributequote it, encode quotes
JS string\xHH or JSON
URLencodeURIComponent + http/https allowlist

3. Layers, in order

  1. Auto-escapetemplates
  2. Sanitiserallowlist, rich HTML
  3. CSPnonce + strict-dynamic
  4. CookiesHttpOnly, Secure, SameSite

4. Audit and interview

bash
rg -n "innerHTML|outerHTML|document\.write|eval|dangerouslySetInnerHTML|v-html|\|safe|mark_safe|bypassSecurityTrust"

sinks to grep in review

  • Comment box? Escape on output, sanitise rich HTML, nonce CSP, HttpOnly cookie
  • Stop and miss? Escaping misses hatches; CSP and HttpOnly contain, they do not stop requests
  • Audit? Grep sinks, trace sources to them, check CSP for unsafe-inline
Web Security (OWASP)Page 7

Check yourself

A page runs el.innerHTML = location.hash.slice(1). Which XSS type is this, and will server logs show the payload?
  • DOM XSS: source location.hash flows into sink innerHTML.
  • The fragment is never sent to the server, so logs see nothing.
  • Fix: use textContent.
A user URL is HTML-escaped, then placed in <a href="...">. The user submits javascript:alert(1). Does it run?
  • Yes, on click: that string has no characters that HTML-escaping changes.
  • Allowlist the scheme (http/https only).
The CSP is script-src 'self' 'unsafe-inline'. An attacker injects an inline <script>. Is it blocked?
  • No: 'unsafe-inline' lets injected inline script run.
  • Use a per-response nonce with 'strict-dynamic' instead.