Stored XSS & safe rendering

Cross-site scripting

Lesson 2 of 20 · 11 min · Module 1: Untrusted input

Cross-site scripting, or XSS, is one of the most common web vulnerabilities. An attacker gets their own HTML or JavaScript into a page that other people view, and it runs with those visitors’ accounts.

Start this lesson →

Free. No installation: you write and run the code in your browser, and it is checked automatically.

By the end, you’ll be able to…

  • Explain how cross-site scripting works
  • Recognise the APIs that turn text into HTML
  • Render untrusted data with textContent and setAttribute

The idea

It happens when text from people is inserted as HTML, usually with innerHTML or insertAdjacentHTML. A comment such as <img src=x onerror=…> becomes a real element, and its code runs for every visitor who opens the page. This is called stored XSS, because the attack is saved on the server. The fix is to build elements yourself and insert people’s text with textContent, which never turns text into HTML.

const comment = '<img src=x onerror="alert(1)">';
box.innerHTML = comment;   // creates an <img> whose code can run
box.textContent = comment; // shows the characters, nothing runs

01What an attacker gains

Script that runs in your page can do everything the visitor can: read what is on the screen, send requests with their session, change their email address or post in their name. One unsafe comment is enough to affect every visitor who opens the page.

02Know the dangerous sinks

innerHTML, outerHTML, insertAdjacentHTML, document.write and the srcdoc attribute all parse HTML. Event attributes such as onclick, and addresses in href or src, are dangerous too. createElement, textContent and setAttribute for ordinary attributes are safe.

element.innerHTML = text;    // parses HTML: dangerous with untrusted text
element.textContent = text;  // always plain text: safe

03Here, the attack is blocked twice

This preview runs under a Content Security Policy that stops inline event handlers, so the injected image cannot run its code here. Many real sites have no such policy, and then it runs. Fix the code first, and treat a policy as a second safety net, as you will see later in this course.

Your turn

Rewrite renderComments(comments) so it never inserts comment data as HTML. For every comment, create an <li class="comment"> with a title of Posted by and the author, containing a <strong class="author"> with the author and a <p class="text"> with the text. Use createElement, textContent and setAttribute.

What the checks look for

  • Every comment is shown
  • HTML in comments stays plain text
  • Quotes cannot break out of an attribute
  • Each comment has the expected structure
  • It works with any comments
Try it in the editor →

Take this with you

Untrusted data goes into the page as text, never as HTML.

Go a little further

Add a form that previews a new comment before it is posted, using the same renderComments function.

Learn Web Security Foundations for free

20 short lessons with real code and instant checks.

See the whole course →