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
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
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.