Skip to content

Commit d03b6ec

Browse files
committedJan 8, 2020
feat: access control to prototype properties via whitelist
Disallow access to prototype properties and methods by default. Access to properties is always checked via `Object.prototype.hasOwnProperty.call(parent, propertyName)`. New runtime options: - **allowedProtoMethods**: a string-to-boolean map of property-names that are allowed if they are methods of the parent object. - **allowedProtoProperties**: a string-to-boolean map of property-names that are allowed if they are properties but not methods of the parent object. ```js const template = handlebars.compile('{{aString.trim}}') const result = template({ aString: ' abc ' }) // result is empty, because trim is defined at String prototype ``` ```js const template = handlebars.compile('{{aString.trim}}') const result = template({ aString: ' abc ' }, { allowedProtoMethods: { trim: true } }) // result = 'abc' ``` Implementation details: The method now "container.lookupProperty" handles the prototype-checks and the white-lists. It is used in - JavaScriptCompiler#nameLookup - The "lookup"-helper (passed to all helpers as "options.lookupProperty") - The "lookup" function at the container, which is used for recursive lookups in "compat" mode Compatibility: - **Old precompiled templates work with new runtimes**: The "options.lookupPropery"-function is passed to the helper by a wrapper, not by the compiled templated. - **New templates work with old runtimes**: The template contains a function that is used as fallback if the "lookupProperty"-function cannot be found at the container. However, the runtime-options "allowedProtoProperties" and "allowedProtoMethods" only work with the newest runtime. BREAKING CHANGE: - access to prototype properties is forbidden completely by default
1 parent 164b7ff commit d03b6ec

12 files changed

+354
-73
lines changed
 

‎lib/handlebars/compiler/javascript-compiler.js

+31-23
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { COMPILER_REVISION, REVISION_CHANGES } from '../base';
22
import Exception from '../exception';
33
import { isArray } from '../utils';
44
import CodeGen from './code-gen';
5-
import { dangerousPropertyRegex } from '../helpers/lookup';
65

76
function Literal(value) {
87
this.value = value;
@@ -13,27 +12,8 @@ function JavaScriptCompiler() {}
1312
JavaScriptCompiler.prototype = {
1413
// PUBLIC API: You can override these methods in a subclass to provide
1514
// alternative compiled forms for name lookup and buffering semantics
16-
nameLookup: function(parent, name /* , type*/) {
17-
if (dangerousPropertyRegex.test(name)) {
18-
const isEnumerable = [
19-
this.aliasable('container.propertyIsEnumerable'),
20-
'.call(',
21-
parent,
22-
',',
23-
JSON.stringify(name),
24-
')'
25-
];
26-
return ['(', isEnumerable, '?', _actualLookup(), ' : undefined)'];
27-
}
28-
return _actualLookup();
29-
30-
function _actualLookup() {
31-
if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
32-
return [parent, '.', name];
33-
} else {
34-
return [parent, '[', JSON.stringify(name), ']'];
35-
}
36-
}
15+
nameLookup: function(parent, name /*, type */) {
16+
return this.internalNameLookup(parent, name);
3717
},
3818
depthedLookup: function(name) {
3919
return [this.aliasable('container.lookup'), '(depths, "', name, '")'];
@@ -69,6 +49,12 @@ JavaScriptCompiler.prototype = {
6949
return this.quotedString('');
7050
},
7151
// END PUBLIC API
52+
internalNameLookup: function(parent, name) {
53+
this.lookupPropertyFunctionIsUsed = true;
54+
return ['lookupProperty(', parent, ',', JSON.stringify(name), ')'];
55+
},
56+
57+
lookupPropertyFunctionIsUsed: false,
7258

7359
compile: function(environment, options, context, asObject) {
7460
this.environment = environment;
@@ -131,7 +117,11 @@ JavaScriptCompiler.prototype = {
131117
if (!this.decorators.isEmpty()) {
132118
this.useDecorators = true;
133119

134-
this.decorators.prepend('var decorators = container.decorators;\n');
120+
this.decorators.prepend([
121+
'var decorators = container.decorators, ',
122+
this.lookupPropertyFunctionVarDeclaration(),
123+
';\n'
124+
]);
135125
this.decorators.push('return fn;');
136126

137127
if (asObject) {
@@ -248,6 +238,10 @@ JavaScriptCompiler.prototype = {
248238
}
249239
});
250240

241+
if (this.lookupPropertyFunctionIsUsed) {
242+
varDeclarations += ', ' + this.lookupPropertyFunctionVarDeclaration();
243+
}
244+
251245
let params = ['container', 'depth0', 'helpers', 'partials', 'data'];
252246

253247
if (this.useBlockParams || this.useDepths) {
@@ -335,6 +329,17 @@ JavaScriptCompiler.prototype = {
335329
return this.source.merge();
336330
},
337331

332+
lookupPropertyFunctionVarDeclaration: function() {
333+
return `
334+
lookupProperty = container.lookupProperty || function(parent, propertyName) {
335+
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
336+
return parent[propertyName];
337+
}
338+
return undefined
339+
}
340+
`.trim();
341+
},
342+
338343
// [blockValue]
339344
//
340345
// On stack, before: hash, inverse, program, value
@@ -1241,6 +1246,9 @@ JavaScriptCompiler.prototype = {
12411246
}
12421247
})();
12431248

1249+
/**
1250+
* @deprecated May be removed in the next major version
1251+
*/
12441252
JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
12451253
return (
12461254
!JavaScriptCompiler.RESERVED_WORDS[name] &&

‎lib/handlebars/helpers/lookup.js

+3-10
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
1-
export const dangerousPropertyRegex = /^(constructor|__defineGetter__|__defineSetter__|__lookupGetter__|__proto__)$/;
2-
31
export default function(instance) {
4-
instance.registerHelper('lookup', function(obj, field) {
2+
instance.registerHelper('lookup', function(obj, field, options) {
53
if (!obj) {
4+
// Note for 5.0: Change to "obj == null" in 5.0
65
return obj;
76
}
8-
if (
9-
dangerousPropertyRegex.test(String(field)) &&
10-
!Object.prototype.propertyIsEnumerable.call(obj, field)
11-
) {
12-
return undefined;
13-
}
14-
return obj[field];
7+
return options.lookupProperty(obj, field);
158
});
169
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { extend } from '../utils';
2+
3+
/**
4+
* Create a new object with "null"-prototype to avoid truthy results on prototype properties.
5+
* The resulting object can be used with "object[property]" to check if a property exists
6+
* @param {...object} sources a varargs parameter of source objects that will be merged
7+
* @returns {object}
8+
*/
9+
export function createNewLookupObject(...sources) {
10+
return extend(Object.create(null), ...sources);
11+
}

‎lib/handlebars/internal/wrapHelper.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export function wrapHelper(helper, transformOptionsFn) {
2+
let wrapper = function(/* dynamic arguments */) {
3+
const options = arguments[arguments.length - 1];
4+
arguments[arguments.length - 1] = transformOptionsFn(options);
5+
return helper.apply(this, arguments);
6+
};
7+
return wrapper;
8+
}

‎lib/handlebars/runtime.js

+51-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
REVISION_CHANGES
88
} from './base';
99
import { moveHelperToHooks } from './helpers';
10+
import { wrapHelper } from './internal/wrapHelper';
11+
import { createNewLookupObject } from './internal/createNewLookupObject';
1012

1113
export function checkRevision(compilerInfo) {
1214
const compilerRevision = (compilerInfo && compilerInfo[0]) || 1,
@@ -69,13 +71,17 @@ export function template(templateSpec, env) {
6971
}
7072
partial = env.VM.resolvePartial.call(this, partial, context, options);
7173

72-
let optionsWithHooks = Utils.extend({}, options, { hooks: this.hooks });
74+
let extendedOptions = Utils.extend({}, options, {
75+
hooks: this.hooks,
76+
allowedProtoMethods: this.allowedProtoMethods,
77+
allowedProtoProperties: this.allowedProtoProperties
78+
});
7379

7480
let result = env.VM.invokePartial.call(
7581
this,
7682
partial,
7783
context,
78-
optionsWithHooks
84+
extendedOptions
7985
);
8086

8187
if (result == null && env.compile) {
@@ -84,7 +90,7 @@ export function template(templateSpec, env) {
8490
templateSpec.compilerOptions,
8591
env
8692
);
87-
result = options.partials[options.name](context, optionsWithHooks);
93+
result = options.partials[options.name](context, extendedOptions);
8894
}
8995
if (result != null) {
9096
if (options.indent) {
@@ -118,10 +124,26 @@ export function template(templateSpec, env) {
118124
}
119125
return obj[name];
120126
},
127+
lookupProperty: function(parent, propertyName) {
128+
let result = parent[propertyName];
129+
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
130+
return result;
131+
}
132+
const whitelist =
133+
typeof result === 'function'
134+
? container.allowedProtoMethods
135+
: container.allowedProtoProperties;
136+
137+
if (whitelist[propertyName] === true) {
138+
return result;
139+
}
140+
return undefined;
141+
},
121142
lookup: function(depths, name) {
122143
const len = depths.length;
123144
for (let i = 0; i < len; i++) {
124-
if (depths[i] && depths[i][name] != null) {
145+
let result = depths[i] && container.lookupProperty(depths[i], name);
146+
if (result != null) {
125147
return depths[i][name];
126148
}
127149
}
@@ -229,7 +251,9 @@ export function template(templateSpec, env) {
229251

230252
ret._setup = function(options) {
231253
if (!options.partial) {
232-
container.helpers = Utils.extend({}, env.helpers, options.helpers);
254+
let mergedHelpers = Utils.extend({}, env.helpers, options.helpers);
255+
wrapHelpersToPassLookupProperty(mergedHelpers, container);
256+
container.helpers = mergedHelpers;
233257

234258
if (templateSpec.usePartial) {
235259
// Use mergeIfNeeded here to prevent compiling global partials multiple times
@@ -247,13 +271,21 @@ export function template(templateSpec, env) {
247271
}
248272

249273
container.hooks = {};
274+
container.allowedProtoProperties = createNewLookupObject(
275+
options.allowedProtoProperties
276+
);
277+
container.allowedProtoMethods = createNewLookupObject(
278+
options.allowedProtoMethods
279+
);
250280

251281
let keepHelperInHelpers =
252282
options.allowCallsToHelperMissing ||
253283
templateWasPrecompiledWithCompilerV7;
254284
moveHelperToHooks(container, 'helperMissing', keepHelperInHelpers);
255285
moveHelperToHooks(container, 'blockHelperMissing', keepHelperInHelpers);
256286
} else {
287+
container.allowedProtoProperties = options.allowedProtoProperties;
288+
container.allowedProtoMethods = options.allowedProtoMethods;
257289
container.helpers = options.helpers;
258290
container.partials = options.partials;
259291
container.decorators = options.decorators;
@@ -405,3 +437,17 @@ function executeDecorators(fn, prog, container, depths, data, blockParams) {
405437
}
406438
return prog;
407439
}
440+
441+
function wrapHelpersToPassLookupProperty(mergedHelpers, container) {
442+
Object.keys(mergedHelpers).forEach(helperName => {
443+
let helper = mergedHelpers[helperName];
444+
mergedHelpers[helperName] = passLookupPropertyOption(helper, container);
445+
});
446+
}
447+
448+
function passLookupPropertyOption(helper, container) {
449+
const lookupProperty = container.lookupProperty;
450+
return wrapHelper(helper, options => {
451+
return Utils.extend({ lookupProperty }, options);
452+
});
453+
}

‎spec/blocks.js

+1
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ describe('blocks', function() {
275275
'Goodbye cruel OMG!'
276276
);
277277
});
278+
278279
it('block with deep recursive pathed lookup', function() {
279280
var string =
280281
'{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}';

‎spec/helpers.js

+11
Original file line numberDiff line numberDiff line change
@@ -1328,4 +1328,15 @@ describe('helpers', function() {
13281328
);
13291329
});
13301330
});
1331+
1332+
describe('the lookupProperty-option', function() {
1333+
it('should be passed to custom helpers', function() {
1334+
expectTemplate('{{testHelper}}')
1335+
.withHelper('testHelper', function testHelper(options) {
1336+
return options.lookupProperty(this, 'testProperty');
1337+
})
1338+
.withInput({ testProperty: 'abc' })
1339+
.toCompileTo('abc');
1340+
});
1341+
});
13311342
});

‎spec/javascript-compiler.js

+25
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,29 @@ describe('javascript-compiler api', function() {
8181
shouldCompileTo('{{foo}}', { foo: 'food' }, 'food_foo');
8282
});
8383
});
84+
85+
describe('#isValidJavaScriptVariableName', function() {
86+
// It is there and accessible and could be used by someone. That's why we don't remove it
87+
// it 4.x. But if we keep it, we add a test
88+
// This test should not encourage you to use the function. It is not needed any more
89+
// and might be removed in 5.0
90+
['test', 'abc123', 'abc_123'].forEach(function(validVariableName) {
91+
it("should return true for '" + validVariableName + "'", function() {
92+
expect(
93+
handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName(
94+
validVariableName
95+
)
96+
).to.be.true();
97+
});
98+
});
99+
[('123test', 'abc()', 'abc.cde')].forEach(function(invalidVariableName) {
100+
it("should return true for '" + invalidVariableName + "'", function() {
101+
expect(
102+
handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName(
103+
invalidVariableName
104+
)
105+
).to.be.false();
106+
});
107+
});
108+
});
84109
});

‎spec/regressions.js

+48-6
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ describe('Regressions', function() {
390390
it('should compile and execute templates', function() {
391391
var newHandlebarsInstance = Handlebars.create();
392392

393-
registerTemplate(newHandlebarsInstance);
393+
registerTemplate(newHandlebarsInstance, compiledTemplateVersion7());
394394
newHandlebarsInstance.registerHelper('loud', function(value) {
395395
return value.toUpperCase();
396396
});
@@ -405,19 +405,39 @@ describe('Regressions', function() {
405405

406406
shouldThrow(
407407
function() {
408-
registerTemplate(newHandlebarsInstance);
408+
registerTemplate(newHandlebarsInstance, compiledTemplateVersion7());
409409
newHandlebarsInstance.templates['test.hbs']({});
410410
},
411411
Handlebars.Exception,
412412
'Missing helper: "loud"'
413413
);
414414
});
415415

416-
// This is a only slightly modified precompiled templated from compiled with 4.2.1
417-
function registerTemplate(Handlebars) {
416+
it('should pass "options.lookupProperty" to "lookup"-helper, even with old templates', function() {
417+
var newHandlebarsInstance = Handlebars.create();
418+
registerTemplate(
419+
newHandlebarsInstance,
420+
compiledTemplateVersion7_usingLookupHelper()
421+
);
422+
423+
newHandlebarsInstance.templates['test.hbs']({});
424+
425+
expect(
426+
newHandlebarsInstance.templates['test.hbs']({
427+
property: 'a',
428+
test: { a: 'b' }
429+
})
430+
).to.equal('b');
431+
});
432+
433+
function registerTemplate(Handlebars, compileTemplate) {
418434
var template = Handlebars.template,
419435
templates = (Handlebars.templates = Handlebars.templates || {});
420-
templates['test.hbs'] = template({
436+
templates['test.hbs'] = template(compileTemplate);
437+
}
438+
439+
function compiledTemplateVersion7() {
440+
return {
421441
compiler: [7, '>= 4.0.0'],
422442
main: function(container, depth0, helpers, partials, data) {
423443
return (
@@ -435,7 +455,29 @@ describe('Regressions', function() {
435455
);
436456
},
437457
useData: true
438-
});
458+
};
459+
}
460+
461+
function compiledTemplateVersion7_usingLookupHelper() {
462+
// This is the compiled version of "{{lookup test property}}"
463+
return {
464+
compiler: [7, '>= 4.0.0'],
465+
main: function(container, depth0, helpers, partials, data) {
466+
return container.escapeExpression(
467+
helpers.lookup.call(
468+
depth0 != null ? depth0 : container.nullContext || {},
469+
depth0 != null ? depth0.test : depth0,
470+
depth0 != null ? depth0.property : depth0,
471+
{
472+
name: 'lookup',
473+
hash: {},
474+
data: data
475+
}
476+
)
477+
);
478+
},
479+
useData: true
480+
};
439481
}
440482
});
441483

‎spec/security.js

+153-29
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ describe('security issues', function() {
1919
.toCompileTo('');
2020
});
2121

22-
it('should allow the "constructor" property to be accessed if it is enumerable', function() {
22+
it('should allow the "constructor" property to be accessed if it is an "ownProperty"', function() {
2323
shouldCompileTo(
2424
'{{constructor.name}}',
2525
{
@@ -40,7 +40,7 @@ describe('security issues', function() {
4040
);
4141
});
4242

43-
it('should allow the "constructor" property to be accessed if it is enumerable', function() {
43+
it('should allow the "constructor" property to be accessed if it is an "own property"', function() {
4444
shouldCompileTo(
4545
'{{lookup (lookup this "constructor") "name"}}',
4646
{
@@ -51,27 +51,6 @@ describe('security issues', function() {
5151
'here we go'
5252
);
5353
});
54-
55-
it('should allow prototype properties that are not constructors', function() {
56-
function TestClass() {}
57-
58-
Object.defineProperty(TestClass.prototype, 'abc', {
59-
get: function() {
60-
return 'xyz';
61-
}
62-
});
63-
64-
shouldCompileTo(
65-
'{{#with this as |obj|}}{{obj.abc}}{{/with}}',
66-
new TestClass(),
67-
'xyz'
68-
);
69-
shouldCompileTo(
70-
'{{#with this as |obj|}}{{lookup obj "abc"}}{{/with}}',
71-
new TestClass(),
72-
'xyz'
73-
);
74-
});
7554
});
7655

7756
describe('GH-1558: Prevent explicit call of helperMissing-helpers', function() {
@@ -171,38 +150,183 @@ describe('security issues', function() {
171150
});
172151

173152
describe('GH-1595', function() {
174-
it('properties, that are required to be enumerable', function() {
153+
it('properties, that are required to be own properties', function() {
175154
expectTemplate('{{constructor}}')
176155
.withInput({})
177156
.toCompileTo('');
157+
178158
expectTemplate('{{__defineGetter__}}')
179159
.withInput({})
180160
.toCompileTo('');
161+
181162
expectTemplate('{{__defineSetter__}}')
182163
.withInput({})
183164
.toCompileTo('');
165+
184166
expectTemplate('{{__lookupGetter__}}')
185167
.withInput({})
186168
.toCompileTo('');
169+
187170
expectTemplate('{{__proto__}}')
188171
.withInput({})
189172
.toCompileTo('');
190173

191-
expectTemplate('{{lookup "constructor"}}')
174+
expectTemplate('{{lookup this "constructor"}}')
192175
.withInput({})
193176
.toCompileTo('');
194-
expectTemplate('{{lookup "__defineGetter__"}}')
177+
178+
expectTemplate('{{lookup this "__defineGetter__"}}')
195179
.withInput({})
196180
.toCompileTo('');
197-
expectTemplate('{{lookup "__defineSetter__"}}')
181+
182+
expectTemplate('{{lookup this "__defineSetter__"}}')
198183
.withInput({})
199184
.toCompileTo('');
200-
expectTemplate('{{lookup "__lookupGetter__"}}')
185+
186+
expectTemplate('{{lookup this "__lookupGetter__"}}')
201187
.withInput({})
202188
.toCompileTo('');
203-
expectTemplate('{{lookup "__proto__"}}')
189+
190+
expectTemplate('{{lookup this "__proto__"}}')
204191
.withInput({})
205192
.toCompileTo('');
206193
});
194+
195+
describe('GH-1631: disallow access to prototype functions', function() {
196+
function TestClass() {}
197+
198+
TestClass.prototype.aProperty = 'propertyValue';
199+
TestClass.prototype.aMethod = function() {
200+
return 'returnValue';
201+
};
202+
203+
describe('control access to prototype methods via "allowedProtoMethods"', function() {
204+
it('should be prohibited by default', function() {
205+
expectTemplate('{{aMethod}}')
206+
.withInput(new TestClass())
207+
.toCompileTo('');
208+
});
209+
210+
it('can be allowed', function() {
211+
expectTemplate('{{aMethod}}')
212+
.withInput(new TestClass())
213+
.withRuntimeOptions({
214+
allowedProtoMethods: {
215+
aMethod: true
216+
}
217+
})
218+
.toCompileTo('returnValue');
219+
});
220+
221+
it('should be prohibited by default (in "compat" mode)', function() {
222+
expectTemplate('{{aMethod}}')
223+
.withInput(new TestClass())
224+
.withCompileOptions({ compat: true })
225+
.toCompileTo('');
226+
});
227+
228+
it('can be allowed (in "compat" mode)', function() {
229+
expectTemplate('{{aMethod}}')
230+
.withInput(new TestClass())
231+
.withCompileOptions({ compat: true })
232+
.withRuntimeOptions({
233+
allowedProtoMethods: {
234+
aMethod: true
235+
}
236+
})
237+
.toCompileTo('returnValue');
238+
});
239+
240+
it('should cause the recursive lookup by default (in "compat" mode)', function() {
241+
expectTemplate('{{#aString}}{{trim}}{{/aString}}')
242+
.withInput({ aString: ' abc ', trim: 'trim' })
243+
.withCompileOptions({ compat: true })
244+
.toCompileTo('trim');
245+
});
246+
247+
it('should not cause the recursive lookup if allowed through options(in "compat" mode)', function() {
248+
expectTemplate('{{#aString}}{{trim}}{{/aString}}')
249+
.withInput({ aString: ' abc ', trim: 'trim' })
250+
.withCompileOptions({ compat: true })
251+
.withRuntimeOptions({
252+
allowedProtoMethods: {
253+
trim: true
254+
}
255+
})
256+
.toCompileTo('abc');
257+
});
258+
});
259+
260+
describe('control access to prototype non-methods via "allowedProtoProperties"', function() {
261+
it('should be prohibited by default', function() {
262+
expectTemplate('{{aProperty}}')
263+
.withInput(new TestClass())
264+
.toCompileTo('');
265+
});
266+
267+
it('can be turned on', function() {
268+
expectTemplate('{{aProperty}}')
269+
.withInput(new TestClass())
270+
.withRuntimeOptions({
271+
allowedProtoProperties: {
272+
aProperty: true
273+
}
274+
})
275+
.toCompileTo('propertyValue');
276+
});
277+
278+
it('should be prohibited by default (in "compat" mode)', function() {
279+
expectTemplate('{{aProperty}}')
280+
.withInput(new TestClass())
281+
.withCompileOptions({ compat: true })
282+
.toCompileTo('');
283+
});
284+
285+
it('can be turned on (in "compat" mode)', function() {
286+
expectTemplate('{{aProperty}}')
287+
.withInput(new TestClass())
288+
.withCompileOptions({ compat: true })
289+
.withRuntimeOptions({
290+
allowedProtoProperties: {
291+
aProperty: true
292+
}
293+
})
294+
.toCompileTo('propertyValue');
295+
});
296+
});
297+
298+
describe('compatibility with old runtimes, that do not provide the function "container.lookupProperty"', function() {
299+
beforeEach(function simulateRuntimeWithoutLookupProperty() {
300+
var oldTemplateMethod = handlebarsEnv.template;
301+
sinon.replace(handlebarsEnv, 'template', function(templateSpec) {
302+
templateSpec.main = wrapToAdjustContainer(templateSpec.main);
303+
return oldTemplateMethod.call(this, templateSpec);
304+
});
305+
});
306+
307+
afterEach(function() {
308+
sinon.restore();
309+
});
310+
311+
it('should work with simple properties', function() {
312+
expectTemplate('{{aProperty}}')
313+
.withInput({ aProperty: 'propertyValue' })
314+
.toCompileTo('propertyValue');
315+
});
316+
317+
it('should work with Array.prototype.length', function() {
318+
expectTemplate('{{anArray.length}}')
319+
.withInput({ anArray: ['a', 'b', 'c'] })
320+
.toCompileTo('3');
321+
});
322+
});
323+
});
207324
});
208325
});
326+
327+
function wrapToAdjustContainer(precompiledTemplateFunction) {
328+
return function templateFunctionWrapper(container /*, more args */) {
329+
delete container.lookupProperty;
330+
return precompiledTemplateFunction.apply(this, arguments);
331+
};
332+
}

‎types/index.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ declare namespace Handlebars {
3030
data?: any;
3131
blockParams?: any[];
3232
allowCallsToHelperMissing?: boolean;
33+
allowedProtoProperties?: { [name: string]: boolean }
34+
allowedProtoMethods?: { [name: string]: boolean }
3335
}
3436

3537
export interface HelperOptions {

‎types/test.ts

+10
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,13 @@ function testExceptionWithNodeTypings() {
240240
let fileName: string = exception.fileName;
241241
let stack: string | undefined = exception.stack;
242242
}
243+
244+
function testProtoPropertyControlOptions() {
245+
Handlebars.compile('test')(
246+
{},
247+
{
248+
allowedProtoMethods: { allowedMethod: true, forbiddenMethod: false },
249+
allowedProtoProperties: { allowedProperty: true, forbiddenProperty: false }
250+
}
251+
);
252+
}

0 commit comments

Comments
 (0)
Please sign in to comment.