Skip to content

Commit 2078c72

Browse files
committed
Disallow calling "helperMissing" and "blockHelperMissing" directly
closes #1558
1 parent fff3e40 commit 2078c72

File tree

6 files changed

+109
-25
lines changed

6 files changed

+109
-25
lines changed

lib/handlebars/compiler/javascript-compiler.js

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ JavaScriptCompiler.prototype = {
311311
// replace it on the stack with the result of properly
312312
// invoking blockHelperMissing.
313313
blockValue: function(name) {
314-
let blockHelperMissing = this.aliasable('helpers.blockHelperMissing'),
314+
let blockHelperMissing = this.aliasable('container.hooks.blockHelperMissing'),
315315
params = [this.contextName(0)];
316316
this.setupHelperArgs(name, 0, params);
317317

@@ -329,7 +329,7 @@ JavaScriptCompiler.prototype = {
329329
// On stack, after, if lastHelper: value
330330
ambiguousBlockValue: function() {
331331
// We're being a bit cheeky and reusing the options value from the prior exec
332-
let blockHelperMissing = this.aliasable('helpers.blockHelperMissing'),
332+
let blockHelperMissing = this.aliasable('container.hooks.blockHelperMissing'),
333333
params = [this.contextName(0)];
334334
this.setupHelperArgs('', 0, params, true);
335335

@@ -622,18 +622,32 @@ JavaScriptCompiler.prototype = {
622622
// If the helper is not found, `helperMissing` is called.
623623
invokeHelper: function(paramSize, name, isSimple) {
624624
let nonHelper = this.popStack(),
625-
helper = this.setupHelper(paramSize, name),
626-
simple = isSimple ? [helper.name, ' || '] : '';
625+
helper = this.setupHelper(paramSize, name);
627626

628-
let lookup = ['('].concat(simple, nonHelper);
627+
let possibleFunctionCalls = [];
628+
629+
if (isSimple) { // direct call to helper
630+
possibleFunctionCalls.push(helper.name);
631+
}
632+
// call a function from the input object
633+
possibleFunctionCalls.push(nonHelper);
629634
if (!this.options.strict) {
630-
lookup.push(' || ', this.aliasable('helpers.helperMissing'));
635+
possibleFunctionCalls.push(this.aliasable('container.hooks.helperMissing'));
631636
}
632-
lookup.push(')');
633637

634-
this.push(this.source.functionCall(lookup, 'call', helper.callParams));
638+
let functionLookupCode = ['(', this.itemsSeparatedBy(possibleFunctionCalls, '||'), ')'];
639+
let functionCall = this.source.functionCall(functionLookupCode, 'call', helper.callParams);
640+
this.push(functionCall);
635641
},
636642

643+
itemsSeparatedBy: function(items, separator) {
644+
let result = [];
645+
result.push(items[0]);
646+
for (let i = 1; i < items.length; i++) {
647+
result.push(separator, items[i]);
648+
}
649+
return result;
650+
},
637651
// [invokeKnownHelper]
638652
//
639653
// On stack, before: hash, inverse, program, params..., ...
@@ -673,7 +687,7 @@ JavaScriptCompiler.prototype = {
673687
lookup[0] = '(helper = ';
674688
lookup.push(
675689
' != null ? helper : ',
676-
this.aliasable('helpers.helperMissing')
690+
this.aliasable('container.hooks.helperMissing')
677691
);
678692
}
679693

lib/handlebars/helpers.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,12 @@ export function registerDefaultHelpers(instance) {
1515
registerLookup(instance);
1616
registerWith(instance);
1717
}
18+
19+
export function moveHelperToHooks(instance, helperName, keepHelper) {
20+
if (instance.helpers[helperName]) {
21+
instance.hooks[helperName] = instance.helpers[helperName];
22+
if (!keepHelper) {
23+
delete instance.helpers[helperName];
24+
}
25+
}
26+
}

lib/handlebars/runtime.js

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as Utils from './utils';
22
import Exception from './exception';
3-
import { COMPILER_REVISION, REVISION_CHANGES, createFrame } from './base';
3+
import {COMPILER_REVISION, createFrame, REVISION_CHANGES} from './base';
4+
import {moveHelperToHooks} from './helpers';
45

56
export function checkRevision(compilerInfo) {
67
const compilerRevision = compilerInfo && compilerInfo[0] || 1,
@@ -21,6 +22,7 @@ export function checkRevision(compilerInfo) {
2122
}
2223

2324
export function template(templateSpec, env) {
25+
2426
/* istanbul ignore next */
2527
if (!env) {
2628
throw new Exception('No environment passed to template');
@@ -42,13 +44,15 @@ export function template(templateSpec, env) {
4244
options.ids[0] = true;
4345
}
4446
}
45-
4647
partial = env.VM.resolvePartial.call(this, partial, context, options);
47-
let result = env.VM.invokePartial.call(this, partial, context, options);
48+
49+
let optionsWithHooks = Utils.extend({}, options, {hooks: this.hooks});
50+
51+
let result = env.VM.invokePartial.call(this, partial, context, optionsWithHooks);
4852

4953
if (result == null && env.compile) {
5054
options.partials[options.name] = env.compile(partial, templateSpec.compilerOptions, env);
51-
result = options.partials[options.name](context, options);
55+
result = options.partials[options.name](context, optionsWithHooks);
5256
}
5357
if (result != null) {
5458
if (options.indent) {
@@ -115,15 +119,6 @@ export function template(templateSpec, env) {
115119
}
116120
return value;
117121
},
118-
merge: function(param, common) {
119-
let obj = param || common;
120-
121-
if (param && common && (param !== common)) {
122-
obj = Utils.extend({}, common, param);
123-
}
124-
125-
return obj;
126-
},
127122
// An empty object to use as replacement for null-contexts
128123
nullContext: Object.seal({}),
129124

@@ -158,19 +153,27 @@ export function template(templateSpec, env) {
158153

159154
ret._setup = function(options) {
160155
if (!options.partial) {
161-
container.helpers = container.merge(options.helpers, env.helpers);
156+
container.helpers = Utils.extend({}, env.helpers, options.helpers);
162157

163158
if (templateSpec.usePartial) {
164-
container.partials = container.merge(options.partials, env.partials);
159+
container.partials = Utils.extend({}, env.partials, options.partials);
165160
}
166161
if (templateSpec.usePartial || templateSpec.useDecorators) {
167-
container.decorators = container.merge(options.decorators, env.decorators);
162+
container.decorators = Utils.extend({}, env.decorators, options.decorators);
168163
}
164+
165+
container.hooks = {};
166+
let keepHelper = options.allowCallsToHelperMissing;
167+
moveHelperToHooks(container, 'helperMissing', keepHelper);
168+
moveHelperToHooks(container, 'blockHelperMissing', keepHelper);
169+
169170
} else {
170171
container.helpers = options.helpers;
171172
container.partials = options.partials;
172173
container.decorators = options.decorators;
174+
container.hooks = options.hooks;
173175
}
176+
174177
};
175178

176179
ret._child = function(i, data, blockParams, depths) {

lib/handlebars/utils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
const escape = {
23
'&': '&amp;',
34
'<': '&lt;',

spec/security.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,60 @@ describe('security issues', function() {
3232

3333
});
3434
});
35+
36+
describe('GH-xxxx: Prevent explicit call of helperMissing-helpers', function() {
37+
if (!Handlebars.compile) {
38+
return;
39+
}
40+
41+
describe('without the option "allowExplicitCallOfHelperMissing"', function() {
42+
it('should throw an exception when calling "{{helperMissing}}" ', function() {
43+
shouldThrow(function() {
44+
var template = Handlebars.compile('{{helperMissing}}');
45+
template({});
46+
}, Error);
47+
});
48+
it('should throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', function() {
49+
shouldThrow(function() {
50+
var template = Handlebars.compile('{{#helperMissing}}{{/helperMissing}}');
51+
template({});
52+
}, Error);
53+
});
54+
it('should throw an exception when calling "{{blockHelperMissing "abc" .}}" ', function() {
55+
var functionCalls = [];
56+
shouldThrow(function() {
57+
var template = Handlebars.compile('{{blockHelperMissing "abc" .}}');
58+
template({ fn: function() { functionCalls.push('called'); }});
59+
}, Error);
60+
equals(functionCalls.length, 0);
61+
});
62+
it('should throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function() {
63+
shouldThrow(function() {
64+
var template = Handlebars.compile('{{#blockHelperMissing .}}{{/blockHelperMissing}}');
65+
template({ fn: function() { return 'functionInData';}});
66+
}, Error);
67+
});
68+
});
69+
70+
describe('with the option "allowCallsToHelperMissing" set to true', function() {
71+
it('should not throw an exception when calling "{{helperMissing}}" ', function() {
72+
var template = Handlebars.compile('{{helperMissing}}');
73+
template({}, {allowCallsToHelperMissing: true});
74+
});
75+
it('should not throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', function() {
76+
var template = Handlebars.compile('{{#helperMissing}}{{/helperMissing}}');
77+
template({}, {allowCallsToHelperMissing: true});
78+
});
79+
it('should not throw an exception when calling "{{blockHelperMissing "abc" .}}" ', function() {
80+
var functionCalls = [];
81+
var template = Handlebars.compile('{{blockHelperMissing "abc" .}}');
82+
template({ fn: function() { functionCalls.push('called'); }}, {allowCallsToHelperMissing: true});
83+
equals(functionCalls.length, 1);
84+
});
85+
it('should not throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function() {
86+
var template = Handlebars.compile('{{#blockHelperMissing true}}sdads{{/blockHelperMissing}}');
87+
template({}, {allowCallsToHelperMissing: true});
88+
});
89+
});
90+
});
3591
});

types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ declare namespace Handlebars {
2929
decorators?: { [name: string]: Function };
3030
data?: any;
3131
blockParams?: any[];
32+
allowCallsToHelperMissing: boolean;
3233
}
3334

3435
export interface HelperOptions {

0 commit comments

Comments
 (0)