describe('security issues', function() { describe('GH-1495: Prevent Remote Code Execution via constructor', function() { it('should not allow constructors to be accessed', function() { expectTemplate('{{lookup (lookup this "constructor") "name"}}') .withInput({}) .toCompileTo(''); expectTemplate('{{constructor.name}}') .withInput({}) .toCompileTo(''); }); it('GH-1603: should not allow constructors to be accessed (lookup via toString)', function() { expectTemplate('{{lookup (lookup this (list "constructor")) "name"}}') .withInput({}) .withHelper('list', function(element) { return [element]; }) .toCompileTo(''); }); it('should allow the "constructor" property to be accessed if it is an "ownProperty"', function() { expectTemplate('{{constructor.name}}') .withInput({ constructor: { name: 'here we go' } }) .toCompileTo('here we go'); expectTemplate('{{lookup (lookup this "constructor") "name"}}') .withInput({ constructor: { name: 'here we go' } }) .toCompileTo('here we go'); }); it('should allow the "constructor" property to be accessed if it is an "own property"', function() { expectTemplate('{{lookup (lookup this "constructor") "name"}}') .withInput({ constructor: { name: 'here we go' } }) .toCompileTo('here we go'); }); }); describe('GH-1558: Prevent explicit call of helperMissing-helpers', function() { if (!Handlebars.compile) { return; } describe('without the option "allowExplicitCallOfHelperMissing"', function() { it('should throw an exception when calling "{{helperMissing}}" ', function() { expectTemplate('{{helperMissing}}').toThrow(Error); }); it('should throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', function() { expectTemplate('{{#helperMissing}}{{/helperMissing}}').toThrow(Error); }); it('should throw an exception when calling "{{blockHelperMissing "abc" .}}" ', function() { var functionCalls = []; expect(function() { var template = Handlebars.compile('{{blockHelperMissing "abc" .}}'); template({ fn: function() { functionCalls.push('called'); } }); }).to.throw(Error); expect(functionCalls.length).to.equal(0); }); it('should throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function() { expectTemplate('{{#blockHelperMissing .}}{{/blockHelperMissing}}') .withInput({ fn: function() { return 'functionInData'; } }) .toThrow(Error); }); }); describe('with the option "allowCallsToHelperMissing" set to true', function() { it('should not throw an exception when calling "{{helperMissing}}" ', function() { var template = Handlebars.compile('{{helperMissing}}'); template({}, { allowCallsToHelperMissing: true }); }); it('should not throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', function() { var template = Handlebars.compile( '{{#helperMissing}}{{/helperMissing}}' ); template({}, { allowCallsToHelperMissing: true }); }); it('should not throw an exception when calling "{{blockHelperMissing "abc" .}}" ', function() { var functionCalls = []; var template = Handlebars.compile('{{blockHelperMissing "abc" .}}'); template( { fn: function() { functionCalls.push('called'); } }, { allowCallsToHelperMissing: true } ); equals(functionCalls.length, 1); }); it('should not throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function() { var template = Handlebars.compile( '{{#blockHelperMissing true}}sdads{{/blockHelperMissing}}' ); template({}, { allowCallsToHelperMissing: true }); }); }); }); describe('GH-1563', function() { it('should not allow to access constructor after overriding via __defineGetter__', function() { if ({}.__defineGetter__ == null || {}.__lookupGetter__ == null) { return this.skip(); // Browser does not support this exploit anyway } expectTemplate( '{{__defineGetter__ "undefined" valueOf }}' + '{{#with __lookupGetter__ }}' + '{{__defineGetter__ "propertyIsEnumerable" (this.bind (this.bind 1)) }}' + '{{constructor.name}}' + '{{/with}}' ) .withInput({}) .toThrow(/Missing helper: "__defineGetter__"/); }); }); describe('GH-1595: dangerous properties', function() { var templates = [ '{{constructor}}', '{{__defineGetter__}}', '{{__defineSetter__}}', '{{__lookupGetter__}}', '{{__lookupSetter__}}', '{{__proto__}}', '{{lookup this "constructor"}}', '{{lookup this "__defineGetter__"}}', '{{lookup this "__defineSetter__"}}', '{{lookup this "__lookupGetter__"}}', '{{lookup this "__lookupSetter__"}}', '{{lookup this "__proto__"}}' ]; templates.forEach(function(template) { describe('access should be denied to ' + template, function() { it('by default', function() { expectTemplate(template) .withInput({}) .toCompileTo(''); }); it(' with proto-access enabled', function() { expectTemplate(template) .withInput({}) .withRuntimeOptions({ allowProtoPropertiesByDefault: true, allowProtoMethodsByDefault: true }) .toCompileTo(''); }); }); }); }); describe('GH-1631: disallow access to prototype functions', function() { function TestClass() {} TestClass.prototype.aProperty = 'propertyValue'; TestClass.prototype.aMethod = function() { return 'returnValue'; }; beforeEach(function() { handlebarsEnv.resetLoggedPropertyAccesses(); }); afterEach(function() { sinon.restore(); }); describe('control access to prototype methods via "allowedProtoMethods"', function() { checkProtoMethodAccess({}); describe('in compat mode', function() { checkProtoMethodAccess({ compat: true }); }); function checkProtoMethodAccess(compileOptions) { it('should be prohibited by default and log a warning', function() { var spy = sinon.spy(console, 'error'); expectTemplate('{{aMethod}}') .withInput(new TestClass()) .withCompileOptions(compileOptions) .toCompileTo(''); expect(spy.calledOnce).to.be.true(); expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/); }); it('should only log the warning once', function() { var spy = sinon.spy(console, 'error'); expectTemplate('{{aMethod}}') .withInput(new TestClass()) .withCompileOptions(compileOptions) .toCompileTo(''); expectTemplate('{{aMethod}}') .withInput(new TestClass()) .withCompileOptions(compileOptions) .toCompileTo(''); expect(spy.calledOnce).to.be.true(); expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/); }); it('can be allowed, which disables the warning', function() { var spy = sinon.spy(console, 'error'); expectTemplate('{{aMethod}}') .withInput(new TestClass()) .withCompileOptions(compileOptions) .withRuntimeOptions({ allowedProtoMethods: { aMethod: true } }) .toCompileTo('returnValue'); expect(spy.callCount).to.equal(0); }); it('can be turned on by default, which disables the warning', function() { var spy = sinon.spy(console, 'error'); expectTemplate('{{aMethod}}') .withInput(new TestClass()) .withCompileOptions(compileOptions) .withRuntimeOptions({ allowProtoMethodsByDefault: true }) .toCompileTo('returnValue'); expect(spy.callCount).to.equal(0); }); it('can be turned off by default, which disables the warning', function() { var spy = sinon.spy(console, 'error'); expectTemplate('{{aMethod}}') .withInput(new TestClass()) .withCompileOptions(compileOptions) .withRuntimeOptions({ allowProtoMethodsByDefault: false }) .toCompileTo(''); expect(spy.callCount).to.equal(0); }); it('can be turned off, if turned on by default', function() { expectTemplate('{{aMethod}}') .withInput(new TestClass()) .withCompileOptions(compileOptions) .withRuntimeOptions({ allowProtoMethodsByDefault: true, allowedProtoMethods: { aMethod: false } }) .toCompileTo(''); }); } it('should cause the recursive lookup by default (in "compat" mode)', function() { expectTemplate('{{#aString}}{{trim}}{{/aString}}') .withInput({ aString: ' abc ', trim: 'trim' }) .withCompileOptions({ compat: true }) .toCompileTo('trim'); }); it('should not cause the recursive lookup if allowed through options(in "compat" mode)', function() { expectTemplate('{{#aString}}{{trim}}{{/aString}}') .withInput({ aString: ' abc ', trim: 'trim' }) .withCompileOptions({ compat: true }) .withRuntimeOptions({ allowedProtoMethods: { trim: true } }) .toCompileTo('abc'); }); }); describe('control access to prototype non-methods via "allowedProtoProperties" and "allowProtoPropertiesByDefault', function() { checkProtoPropertyAccess({}); describe('in compat-mode', function() { checkProtoPropertyAccess({ compat: true }); }); describe('in strict-mode', function() { checkProtoPropertyAccess({ strict: true }); }); function checkProtoPropertyAccess(compileOptions) { it('should be prohibited by default and log a warning', function() { var spy = sinon.spy(console, 'error'); expectTemplate('{{aProperty}}') .withInput(new TestClass()) .withCompileOptions(compileOptions) .toCompileTo(''); expect(spy.calledOnce).to.be.true(); expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/); }); it('can be explicitly prohibited by default, which disables the warning', function() { var spy = sinon.spy(console, 'error'); expectTemplate('{{aProperty}}') .withInput(new TestClass()) .withCompileOptions(compileOptions) .withRuntimeOptions({ allowProtoPropertiesByDefault: false }) .toCompileTo(''); expect(spy.callCount).to.equal(0); }); it('can be turned on, which disables the warning', function() { var spy = sinon.spy(console, 'error'); expectTemplate('{{aProperty}}') .withInput(new TestClass()) .withCompileOptions(compileOptions) .withRuntimeOptions({ allowedProtoProperties: { aProperty: true } }) .toCompileTo('propertyValue'); expect(spy.callCount).to.equal(0); }); it('can be turned on by default, which disables the warning', function() { var spy = sinon.spy(console, 'error'); expectTemplate('{{aProperty}}') .withInput(new TestClass()) .withCompileOptions(compileOptions) .withRuntimeOptions({ allowProtoPropertiesByDefault: true }) .toCompileTo('propertyValue'); expect(spy.callCount).to.equal(0); }); it('can be turned off, if turned on by default', function() { expectTemplate('{{aProperty}}') .withInput(new TestClass()) .withCompileOptions(compileOptions) .withRuntimeOptions({ allowProtoPropertiesByDefault: true, allowedProtoProperties: { aProperty: false } }) .toCompileTo(''); }); } }); describe('compatibility with old runtimes, that do not provide the function "container.lookupProperty"', function() { beforeEach(function simulateRuntimeWithoutLookupProperty() { var oldTemplateMethod = handlebarsEnv.template; sinon.replace(handlebarsEnv, 'template', function(templateSpec) { templateSpec.main = wrapToAdjustContainer(templateSpec.main); return oldTemplateMethod.call(this, templateSpec); }); }); afterEach(function() { sinon.restore(); }); it('should work with simple properties', function() { expectTemplate('{{aProperty}}') .withInput({ aProperty: 'propertyValue' }) .toCompileTo('propertyValue'); }); it('should work with Array.prototype.length', function() { expectTemplate('{{anArray.length}}') .withInput({ anArray: ['a', 'b', 'c'] }) .toCompileTo('3'); }); }); }); describe('escapes template variables', function() { it('in compat mode', function() { expectTemplate("{{'a\\b'}}") .withCompileOptions({ compat: true }) .withInput({ 'a\\b': 'c' }) .toCompileTo('c'); }); it('in default mode', function() { expectTemplate("{{'a\\b'}}") .withCompileOptions() .withInput({ 'a\\b': 'c' }) .toCompileTo('c'); }); it('in default mode', function() { expectTemplate("{{'a\\b'}}") .withCompileOptions({ strict: true }) .withInput({ 'a\\b': 'c' }) .toCompileTo('c'); }); }); describe('GHSA-2qvq-rjwj-gvw9: partial resolution must not use polluted prototypes', function() { if (!Handlebars.compile) { return; } afterEach(function() { delete Object.prototype.widget; }); it('should not resolve partial names from Object.prototype', function() { // eslint-disable-next-line no-extend-native Object.prototype.widget = ''; expect(function() { Handlebars.compile('
{{> widget}}
')({}); }).to.throw(/could not be found/); }); }); describe('GHSA-2w6w-674q-4c4q, GHSA-xhpv-hc6g-r9c6, GHSA-3mfm-83xf-c92r: untrusted AST inputs', function() { if (!Handlebars.compile) { return; } function createInjectedProgram() { return { type: 'Program', body: [ { type: 'MustacheStatement', escaped: true, strip: { open: false, close: false }, path: { type: 'PathExpression', data: false, depth: 0, parts: ['lookup'], original: 'lookup' }, params: [ { type: 'PathExpression', data: false, depth: 0, parts: [], original: 'this' }, { type: 'NumberLiteral', value: '{},{})) + (Function) + (({}', original: 1 } ] } ] }; } it('should reject AST NumberLiteral type confusion in compile()', function() { expect(function() { var template = Handlebars.compile(createInjectedProgram()); template({}); }).to.throw(/Invalid AST/); }); it('should reject AST objects passed via dynamic partial lookup', function() { expect(function() { var template = Handlebars.compile('{{> (lookup . "payload")}}'); template({ payload: createInjectedProgram() }); }).to.throw(/Invalid AST|could not be found/); }); }); describe('GHSA-442j-39wm-28r2: lookup must return checked value', function() { it('should use the validated value from lookupProperty() in compat mode', function() { var input = { child: {} }; var readCount = 0; Object.defineProperty(input, 'unstable', { enumerable: true, get: function() { readCount++; return readCount === 1 ? 'first-read' : 'second-read'; } }); expectTemplate('{{#with child}}{{unstable}}{{/with}}') .withInput(input) .withCompileOptions({ compat: true }) .toCompileTo('first-read'); }); }); describe('GHSA-9cx6-37pm-9jff: malformed decorators should fail safely', function() { if (!Handlebars.compile) { return; } it('should throw a controlled error for unknown decorators', function() { var template = Handlebars.compile('{{*notRegistered}}'); expect(function() { template({}); }).to.throw(/Missing decorator|not registered/); }); }); describe('GHSA-new: @partial-block must not resolve from polluted prototype', function() { if (!Handlebars.compile) { return; } afterEach(function() { delete Object.prototype['partial-block']; }); it('should not resolve @partial-block from Object.prototype', function() { // eslint-disable-next-line no-extend-native Object.prototype['partial-block'] = ''; expect(function() { Handlebars.compile('{{> @partial-block}}')({}); }).to.throw(/could not be found/); }); it('should not resolve @partial-block from Object.prototype inside a partial', function() { // eslint-disable-next-line no-extend-native Object.prototype['partial-block'] = ''; Handlebars.registerPartial('testPartial', '{{> @partial-block}}'); try { expect(function() { Handlebars.compile('{{> testPartial}}')({}); }).to.throw(/could not be found/); } finally { Handlebars.unregisterPartial('testPartial'); } }); it('should still render legitimate @partial-block content', function() { Handlebars.registerPartial('wrapper', '
{{> @partial-block}}
'); try { var result = Handlebars.compile('{{#> wrapper}}hello{{/wrapper}}')({}); expect(result).to.equal('
hello
'); } finally { Handlebars.unregisterPartial('wrapper'); } }); }); }); function wrapToAdjustContainer(precompiledTemplateFunction) { return function templateFunctionWrapper(container /*, more args */) { delete container.lookupProperty; return precompiledTemplateFunction.apply(this, arguments); }; }