toolhq.io

All posts
5 min readby Jameel Haider

When to escape HTML: XSS basics

Cross site scripting happens when data a user controls ends up in a page as executable markup or script instead of inert text. Someone sets their display name to <script>steal()</script>, your template drops it straight into the HTML, and every visitor runs it. The fix is not a clever filter. It is escaping the data for the exact place it lands, every time it lands there.

Escape on output, not input

A tempting but wrong instinct is to sanitize data as it arrives and store the clean version. This fails for two reasons. First, you do not know at input time where the value will later appear, and the correct escaping depends entirely on that destination. Second, escaped data in your database corrupts everything that is not HTML: exports, APIs, emails, and search all now contain &lt; where a < should be.

Store the raw value. Escape it at the moment you render it, for the context you are rendering into. The same name might be written three different ways on three different pages, and that is correct, because each page is a different context.

Context decides the encoding

The word escape is shorthand for several different transformations. Putting HTML escaping where JavaScript escaping belongs does nothing useful and can still be exploitable.

HTML text context is the body of an element. Escape &, <, and >.

<p>Hello, &lt;user input here&gt;</p>

Attribute context is inside a tag's quoted value. Also escape the quote character that delimits the value, and always quote your attributes.

<input value="&quot;user input&quot;">

An unquoted attribute is dangerous because a space or > in the data can break out into new attributes, including an onerror handler.

JavaScript context is inside a <script> block or an event handler. HTML escaping does not help here; you need JavaScript string escaping, and the only safe habit is to avoid injecting into script at all. Pass data through a quoted HTML attribute or a data- attribute and read it with the DOM, or serialize it as JSON with the right escaping.

URL context is inside an href or src. Percent encode the value, and reject dangerous schemes like javascript: outright. See URL encoding explained for the mechanics.

The rule: the encoding must match the context, and a value crossing from one context into another must be re encoded for the new one.

Frameworks escape for you, mostly

Modern templating does the right thing by default. React escapes any value you interpolate as text, Vue and Angular escape their bindings, and template engines like Jinja and ERB autoescape HTML. You usually get safety for free.

The danger is the escape hatch. dangerouslySetInnerHTML in React, v-html in Vue, innerHTML in plain DOM, and the raw filters in template engines all bypass the automatic encoding. Any user data flowing through those needs either a trusted HTML sanitizer or manual escaping. A strong Content Security Policy is a valuable second layer, limiting what injected script can do if something slips through, but it is defense in depth, not a substitute for encoding.

To see what escaping actually produces for a given string, paste it into the HTML entities encoder and decoder; it runs in your browser, so the input never leaves your device. The habit worth building: keep raw data raw, escape at the point of output, and let the destination context pick the encoding. For the entity formats themselves, see named vs numeric HTML entities.