What XSS is
Why it matters A page that shows text from visitors (comments, reviews, names, search words) can be turned against everyone who reads it, if that text is allowed to become part of the page's code. This bug is called XSS, and it is one of the most common web bugs there is. The fix is small, and after this module you will apply it without thinking.
An analogy: notes passed to the stage
Picture a theatre where the audience can pass notes to the actors, and the actors read them out. That is fun, until one note says: "Stage direction: the actor now hands their keys to row 5." If the actor treats the note as part of the script instead of as something to read out, the play has been taken over by a stranger.
A web page is the script. Text that visitors type is the note. XSS happens when the browser is handed that note in a way that makes it look like part of the script.
HTML: the language a page is written in. Words in angle brackets, called tags, such as <strong> or <img>, tell the browser what to build. Everything else is plain text to show.
Untrusted text: anything your page did not write itself: what a visitor typed, a value in the address, a row in your database that a visitor once filled in.
XSS (cross-site scripting): a bug where untrusted text reaches a page in a way that lets it run as script there. The odd name is historical; what matters is "someone else's code runs inside your page".
Our practice guestbook
Everything in this module happens in a toy browser written in Python, in watched.leg3c_toypages. It reads HTML the way a browser would, finds the elements and text, and notes where a script would have run. It runs no real JavaScript, sends nothing anywhere, and our toy pages have no scripts of their own. So if the toy says script_ran, the script came from visitor text.
from watched.leg3c_toypages import Guestbook
book = Guestbook() # shows entries as they were typed
book.post("Ana", "Lovely campsite!")
book.post("Ben", "<script>alert(1)</script>")
page = book.visit()
print(page.html)
# <h1>Guestbook</h1><ul class="book"><li><strong>Ana</strong>: Lovely campsite!</li>
# <li><strong>Ben</strong>: <script>alert(1)</script></li></ul>
print(page.script_ran, page.ran) # True ['a <script> element']Ben's message went straight into the HTML, so the browser saw a real <script> tag and would run it for every visitor who opens the guestbook. alert(1) just pops up a box; it is the harmless way testers prove a script ran. A real attacker would write something worse.
The fix: escaping
Escaping: replacing the few characters that have a special meaning in HTML with safe codes, called entities, that the browser shows as the character but never treats as code. < becomes <, so the visitor sees a < on screen and no tag is ever made.
Python's standard library has an escaper: html.escape. (In Lesson 60 you will write your own, to see exactly what it does.)
import html
print(html.escape("<script>alert(1)</script>"))
# <script>alert(1)</script>The page now shows Ben's message as text, angle brackets and all. Ben is not silenced, he is just not in charge of the page.
The lab
Write the guestbook's rendering yourself, safely: render_entry(name, message) and render_book(entries). Then load your page in the toy browser and check script_ran is False.
A common misconception
"I will clean the text when it is saved." Better: store what was typed, and escape when you show it. The same text may later go into an email, a JSON reply or a PDF, and each of those needs its own kind of escaping. Escaping on output, in the place where the text meets the HTML, is the habit that holds up.
Try it After you pass, change render_entry to skip html.escape for the message only, and run your code again. The name stays safe, the message does not: every piece of untrusted text needs escaping, not just most of them.
Want to go deeper? PortSwigger's free Web Security Academy has a full XSS track with its own practice labs, built for learning safely.
In other languages
The same fix elsewhere. In the browser, the safe choice is textContent, which always treats text as text.
// JavaScript in the browser
li.textContent = message; // safe: shown as text
li.innerHTML = message; // unsafe: parsed as HTML
// PHP
echo "<li>" . htmlspecialchars($message, ENT_QUOTES) . "</li>";
// Go
fmt.Fprintf(w, "<li>%s</li>", html.EscapeString(message))
// C# (ASP.NET)
var safe = System.Net.WebUtility.HtmlEncode(message);Recap
- XSS: untrusted text becomes part of a page's code and runs for whoever opens the page.
alert(1)is only a harmless proof; a real script can do anything the page can.- Escaping turns special characters into entities, so text is shown, never run.
- Store what was typed; escape every piece of it when you put it into HTML.
Write render_entry(name, message), which returns one guestbook row as <li><strong>NAME</strong>: MESSAGE</li> with both name and message escaped using html.escape. Then write render_book(entries), which takes a list of (name, message) pairs and returns <ul class="book"> + every row + </ul>. Plain text must come out unchanged: render_entry("Ana", "Hi!") is <li><strong>Ana</strong>: Hi!</li>.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.