Escaping done right
Why it matters Every template system and every framework has an escaper inside it. Writing one yourself, once, shows you exactly which characters matter and why the order of the steps matters. Then, when you see an escaper switched off or used in the wrong place, you will spot it.
An analogy: a label maker
A label maker prints whatever you type. If someone types the printer's own control code, a careless label maker would obey it. A careful one prints a picture of that code instead. An HTML escaper is the careful label maker: special characters are printed, never obeyed.
Entity: a code HTML uses to show a character safely. It starts with & and ends with ;. < shows <, & shows &.
The five characters
| Character | Becomes | Why it is special |
|---|---|---|
& | & | starts an entity |
< | < | starts a tag |
> | > | ends a tag |
" | " | ends a double-quoted attribute |
' | ' | ends a single-quoted attribute |
Order matters: ampersand first
If you replace < first, you get <. If you then replace &, that turns into &lt;, and the visitor sees the text < instead of <. So replace & first, then the rest.
"<".replace("<", "<").replace("&", "&") # '&lt;' wrong: escaped twice
"<".replace("&", "&").replace("<", "<") # '<' rightWhy quotes matter: attributes
Text does not only go between tags. It often goes inside an attribute, such as a search box that remembers what you typed:
template = '<input name="q" value="{}">'
typed = 'tent" onfocus="alert(1)'
print(template.format(typed))
# <input name="q" value="tent" onfocus="alert(1)"> the quote ended the value earlyNo angle bracket was needed. The " closed the value and the rest became a new attribute. Escaping " to " keeps it all inside the value. Escape ' too, because some templates use single quotes. And always put quotes around attribute values: without them, a single space is enough to start a new attribute.
What escaping does not cover
HTML escaping is the right tool for text between tags and inside quoted attributes. It is the wrong tool inside a <script> block, inside CSS, and for a link address: href="javascript:..." contains no special characters at all. For links, allow only addresses that start with https:// or http:// (or are relative). For data a script needs, put it in a data- attribute or a JSON block your framework builds for you (Django's json_script, for example).
The exercise
Write escape_html(text). Do not use html.escape here: the point is to build it. The tests try it between tags, in double- and single-quoted attributes, and on hundreds of random strings.
A common misconception
"Escaping twice is extra safe." It is not safe, it is broken: the visitor sees &lt; junk on screen. Escape exactly once, at the moment text goes into the HTML.
Try it After you pass, print escape_html("Fish & chips <3"), then paste the result into load("<p>" + ... + "</p>").text. You get the original words back: escaping is lossless.
In other languages
// JavaScript
function escapeHtml(text) {
return String(text)
.replaceAll("&", "&") // first
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
// Rust (by hand, one pass)
fn escape_html(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
// PHP has it built in
htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, "UTF-8");Recap
- Escape five characters:
&,<,>,"and'. - Replace
&first, or you escape your own entities twice. - Quotes matter because text often lands inside attributes; always quote attribute values.
- HTML escaping is not the tool for script blocks, CSS or link addresses.
Write escape_html(text) without html.escape. Turn the input into a string with str(), then replace & with & first, then < with <, > with >, " with " and ' with '. Return the result.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.