Lesson 3 of 3 in Cross-Site Scripting
Escape On The Way Out
The fix is not to hunt for bad input on the way in — it is to encode every value for the exact place it lands on the way out. Get the context right and the browser reads your data as text, never as code.
3 min read
Not yet reviewed
So I strip <script> from every post before I save it, and the bug is gone?
You just watched an <img onerror> steal a session without a <script> in sight. Blocklists lose this game — the HTML parser knows more tricks than your filter does. You fix it on the way *out*, not the way in.
Encode for the place it lands
The title in the lab was safe and the body was not, and the difference was one function. Before a value is written into HTML, replace the characters that mean something to the parser — < becomes <, > becomes >, & becomes &, and inside an attribute, " and ' too. Now the browser shows <img …> as the literal text a user typed, because it never sees a tag at all.
Concatenated into HTML
page += "<div>" + comment.body + "</div>"Encoded for the HTML context
page += "<div>" + escapeHtml(comment.body) + "</div>"Most frameworks do this for you — a templating engine escapes by default, and you have to go out of your way (a "render this raw" call) to turn it off. The bug usually lives exactly where someone did.
The word context matters because the escaping is different in each place. A value dropped into HTML text, into an attribute, into a URL, or into a <script> block each needs its own encoding — HTML-escaping is wrong inside a URL, and useless inside a script. Encode for the context the value actually lands in.
Why filtering the input loses
Blocking "dangerous" input feels like the fix and is not, for the same reason it failed against injection: you are trying to enumerate every way to say something the parser will accept, and the parser is more creative than you. <img onerror>, <svg onload>, <a href=javascript:>, an uppercase tag, a broken-up keyword — the list has no end. Output encoding does not enumerate anything; it neutralises every character at the one moment it would matter.
A Content-Security-Policy that forbids inline scripts would have blocked the lab's payload from running. So is CSP the fix?
What each layer really buys
Output encoding the fix — data can no longer become markup
HttpOnly cookie loot control — script can't read the cookie
Content-Security containment — an injected script won't execute
Input filtering theatre — the parser knows more tricks than you
- Line 1The only line that removes the vulnerability. The other two limit the damage of one that slipped through; the last does neither.
- Line 2
HttpOnlyhid the cookie, so the attacker acted as the victim directly instead. It narrowed the loot; it did not close the hole.