Rendering user data
Published
Template escaping is the rare guard that is genuinely binary in most people's heads: either the engine escapes or it does not. The measurement below found a third state that is worse than either, because it looks exactly like the safe one. Escaping ran on every interpolation, the count of escaped values did not change, the obvious payload was still neutralised, and the attacker still got script execution.
What the sample actually does
The file configures a view profile with four settings: escaping on, a caller-supplied escaper that handles quotes as well as angle brackets, strict variables on so a missing name is an error rather than an empty string, and the two prototype-access settings pinned to the values the engine already uses. It renders a profile template with three interpolation points - a heading, a single-quoted title attribute and a paragraph - from attacker-controlled data.
The attacker data is aimed at two different sinks on purpose. The display name is a script tag, which is what everybody tests. The tagline is x' onmouseover='steal(), which is aimed at the attribute, and it is the one that separates a good escaper from a weak one. A second, benign data set renders alongside it so that every arm shows what an ordinary page did as well as what the hostile one did.
Two more templates cover the remaining options: one names a field nobody supplied, which is what strict variables is for, and one reaches for an inherited property, which the prototype settings govern. That one renders under a second profile with strict variables off, so the prototype decision is visible in the HTML instead of being swallowed by the unknown-variable error.
The engine is copied unprotected. Its defaults follow the engine-level convention rather than the framework one: no escaping unless the caller asks, its own escaper if the caller does not supply one, and no strict variables. Protection alone, on all five presets, produced behaviour identical to the unprotected file.
Renaming the escaper is worse than it sounds
This is the result worth the whole article. Renaming escapeFn alone left escaping switched on and running. The escaped-value count stayed at four on every render. The reported escaper changed from caller-supplied to engine-builtin. The script tag in the display name was still neutralised and still rendered as visible text.
And the attribute broke out. Our detector for that went from false to true, because the engine's built-in escaper handles ampersands, less-than and greater-than, and does not touch quotes. The tagline closed its own title attribute and an event handler followed it into the tag. The page is now executing attacker script, while every diagnostic that asks "is escaping enabled" answers yes and every test that renders a script tag and looks for it in the output still passes.
That combination is unusual and worth naming. Most of the failures we have measured in this series are a guard disappearing, which at least leaves a clean before-and-after. This one is a guard being silently downgraded to a weaker implementation of itself. There is no error, no count that moves, and the only visible difference is in a context - inside an attribute - that a lot of test suites never exercise.
If you supply a custom escaper anywhere, that is the name to protect first. The reason you supplied it is that the default was not sufficient for your contexts, and losing the name puts you back on the default without telling you.
Renaming autoescape is the loud version
Renaming autoescape did what you would expect and it is the easier failure. Escaped interpolations went from four to zero, raw interpolations from zero to four, the reported escaper became none, and both of our detectors fired: the script tag executed and the attribute broke out. The hostile HTML contains a literal <script> element fetching a URL with the document cookie appended.
It is the loud version only in the sense that any XSS test at all would catch it. Nothing in the render threw, nothing was logged, and the benign page rendered identically to before - the same visible output, just arrived at without escaping. An application whose test suite renders only trusted data would see a completely green run.
Renaming the pair together produced the same result as renaming the switch alone, which is the expected dead-code interaction: once nothing is being escaped, which escaper you would have used stops mattering. It is worth measuring anyway, because the arms where a pair is redundant and the arms where a pair is required are not distinguishable by reading the option names.
The variable-strictness arm, and the two prototype controls
Renaming strictVars turned the misspelled-variable template from an unknown-variable error into a successful render of an empty string. That is a correctness failure rather than a security one in most applications, but it is worth knowing which way it fails: a typo that used to stop the page now produces a page with a hole in it, and holes in rendered output are noticed by users long before they are noticed by monitoring.
The two prototype-access settings were pinned to the values the engine already uses, and renaming either produced no change on either output target. That is the expected result for a control arm, and the caveat is the same as everywhere else in this series: an application that had deliberately turned prototype access on, which some do to expose helper methods to templates, would find the same rename turning working templates into blank output.
Renaming the data is loud, which is the useful contrast
The template paths in this sample live inside string literals, because that is what templates are. String literals are not renamed. So when we renamed the data keys instead of the option names - the display name, tagline, biography and plan fields on the context object - the templates kept asking for names that no longer existed.
Every lookup missed. With strict variables on, the render raised an unknown-variable error and produced empty HTML for both the hostile and the benign data. The page did not render at all. That is the loudest failure in the whole pass, and it is loud for a structural reason worth internalising: the template is a string, the data is an object, and renaming moves one without the other.
The contrast is the lesson. Renaming the option names of a template engine is silent and dangerous. Renaming the data those templates read is immediate and obvious. If you are scoping a member pattern, the data keys will announce themselves in the first render; the option names will not announce themselves at all.
What to do about it
Exclude the option names your template engine reads, and exclude the property names on any context object whose keys appear inside template strings. The second one you will find in seconds because the page goes blank. The first one you will not find without looking for it.
If you supply a custom escaper, treat its option name as security-relevant configuration rather than as a rendering detail, because the fallback is a working escaper that is weaker than yours in exactly the context you added yours for.
Test in the attribute context, not just in element text. A test that renders x' onmouseover='alert(1) into a single-quoted attribute and asserts the attribute did not close catches both the escaper arm and the autoescape arm. A test that renders a script tag into a paragraph catches only one of them, and it is the one you were less likely to have.
Frequently asked questions
Does obfuscation turn off template escaping?
Not by itself. Protection alone, on all five presets we tested, rendered identically to the unprotected file. The results in this article required member renaming pointed at the option names the engine reads.
What was the most surprising result you measured here?
Renaming only the custom escaper. Escaping stayed on, the escaped-value count did not move, and the script-tag payload was still neutralised - but the engine's built-in escaper does not handle quotes, so a value interpolated into a single-quoted attribute closed that attribute and added an event handler.
Why would an engine's built-in escaper not escape quotes?
Because it was written for element text, where angle brackets and ampersands are the characters that matter. Escaping quotes is what makes a value safe inside an attribute, which is why applications that render into attributes often supply a stricter escaper of their own.
Would our existing XSS tests have caught this?
The autoescape arm, almost certainly. The escaper arm, only if you test the attribute context. In our measurement the script-tag payload rendered as harmless text in that arm while the attribute payload executed, so a suite built around script tags reports green.
What happens if the template data keys get renamed instead of the options?
The page stops rendering. Template paths live in string literals, which are not renamed, so the lookups miss and strict variables turns that into an error and empty output. It is the loudest failure we measured this pass, which makes it the one you do not need to plan for.
Does turning strict variables off cause a security problem?
In our measurement it caused a correctness problem rather than a security one: a misspelled variable rendered as an empty string instead of raising. That tends to be noticed by users rather than by monitoring, so it is worth excluding along with the rest.
What should I exclude from member renaming here?
The engine's option names - the escaping switch, any custom escaper, the strict-variable setting and the prototype-access settings - plus the keys of any context object your templates name by string path.
Related reading