Injection and output

Prevent cross-site scripting

Keep untrusted data as text, use framework escaping, and avoid browser sinks that interpret attacker-controlled strings as code or markup.

XSS happens when the browser interprets untrusted data as active page content instead of inert text. An attacker’s string stops being a name and becomes running code. The safest default is to render text as text.

The two lines below look almost identical, but they behave completely differently.

message.textContent = userMessage  // rendered as text, always safe
message.innerHTML = userMessage     // parsed as HTML, a script sink

textContent writes characters. innerHTML parses markup. A sink is any place a value turns back into code, and the same variable can be safe in one sink and dangerous in another.

Match the encoding to the destination

Framework interpolation escapes for you in the normal case. Trust it, and avoid the bypasses.

<span>{userMessage}</span>          {/* React escapes this automatically */}
<span dangerouslySetInnerHTML={{ __html: userMessage }} /> {/* bypass, do not */}

If the product genuinely must accept HTML, run it through a maintained sanitizer with a narrow allowlist rather than writing your own filter.

import DOMPurify from 'dompurify'
preview.innerHTML = DOMPurify.sanitize(userMessage) // strips scripts and handlers

A profile name containing <img src=x onerror=alert(1)> is safe in escaped text. The same value becomes active when a preview feature assigns it to innerHTML.

Escaping once at input time is fragile because the value may later enter HTML, an attribute, or a URL. Keep the stored value unchanged and use a safe sink for its final context.

Trace one user-controlled value to every place it renders and record each sink. Test text, markup, attribute-breaking, and javascript: payloads, then prove none execute in the browser.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →