diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index fcf71a2b..adb1547a 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -4,7 +4,7 @@ import {registerDefaultHelpers} from './helpers'; import {registerDefaultDecorators} from './decorators'; import logger from './logger'; -export const VERSION = '4.5.1'; +export const VERSION = '4.5.2'; export const COMPILER_REVISION = 8; export const LAST_COMPATIBLE_COMPILER_REVISION = 7; diff --git a/lib/handlebars/helpers/lookup.js b/lib/handlebars/helpers/lookup.js index dcd58517..0654cc39 100644 --- a/lib/handlebars/helpers/lookup.js +++ b/lib/handlebars/helpers/lookup.js @@ -3,7 +3,7 @@ export default function(instance) { if (!obj) { return obj; } - if (field === 'constructor' && !obj.propertyIsEnumerable(field)) { + if (String(field) === 'constructor' && !obj.propertyIsEnumerable(field)) { return undefined; } return obj[field]; diff --git a/release-notes.md b/release-notes.md index 11b1d014..3eefabe8 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,7 +2,18 @@ ## Development -[Commits](https://github.com/wycats/handlebars.js/compare/v4.5.1...master) +[Commits](https://github.com/wycats/handlebars.js/compare/v4.5.2...master) + +## v4.5.2 - November 13th, 2019 +# Bugfixes + +- fix: use String(field) in lookup when checking for "constructor" - d541378 +- test: add fluent API for testing Handlebars - c2ac79c + +Compatibility notes: +- no incompatibility are to be expected + +[Commits](https://github.com/wycats/handlebars.js/compare/v4.5.1...v4.5.2) ## v4.5.1 - October 29th, 2019 Bugfixs diff --git a/spec/.eslintrc b/spec/.eslintrc index 421bd00d..34319f8c 100644 --- a/spec/.eslintrc +++ b/spec/.eslintrc @@ -13,6 +13,7 @@ "shouldCompileTo": true, "shouldCompileToWithPartials": true, "shouldThrow": true, + "expectTemplate": true, "compileWithPartials": true, "console": true, "require": true, diff --git a/spec/env/common.js b/spec/env/common.js index 14d302aa..9523eaeb 100644 --- a/spec/env/common.js +++ b/spec/env/common.js @@ -1,4 +1,6 @@ -var global = (function() { return this; }()); +var global = (function() { + return this; +}()); var AssertError; if (Error.captureStackTrace) { @@ -16,10 +18,16 @@ if (Error.captureStackTrace) { AssertError = Error; } +/** + * @deprecated Use "expectTemplate(template)...toCompileTo(output)" instead + */ global.shouldCompileTo = function(string, hashOrArray, expected, message) { shouldCompileToWithPartials(string, hashOrArray, false, expected, message); }; +/** + * @deprecated Use "expectTemplate(template)...toCompileTo(output)" instead + */ global.shouldCompileToWithPartials = function shouldCompileToWithPartials(string, hashOrArray, partials, expected, message) { var result = compileWithPartials(string, hashOrArray, partials); if (result !== expected) { @@ -27,6 +35,9 @@ global.shouldCompileToWithPartials = function shouldCompileToWithPartials(string } }; +/** + * @deprecated Use "expectTemplate(template)...toCompileTo(output)" instead + */ global.compileWithPartials = function(string, hashOrArray, partials) { var template, ary, @@ -36,8 +47,8 @@ global.compileWithPartials = function(string, hashOrArray, partials) { delete hashOrArray.hash; } else if (Object.prototype.toString.call(hashOrArray) === '[object Array]') { ary = []; - ary.push(hashOrArray[0]); - ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] }); + ary.push(hashOrArray[0]); // input + ary.push({helpers: hashOrArray[1], partials: hashOrArray[2]}); options = typeof hashOrArray[3] === 'object' ? hashOrArray[3] : {compat: hashOrArray[3]}; if (hashOrArray[4] != null) { options.data = !!hashOrArray[4]; @@ -75,3 +86,72 @@ global.shouldThrow = function(callback, type, msg) { throw new AssertError('It failed to throw', shouldThrow); } }; + + +global.expectTemplate = function(templateAsString) { + return new HandlebarsTestBench(templateAsString); +}; + + +function HandlebarsTestBench(templateAsString) { + this.templateAsString = templateAsString; + this.helpers = {}; + this.partials = {}; + this.input = {}; + this.message = 'Template' + templateAsString + ' does not evaluate to expected output'; + this.compileOptions = {}; + this.runtimeOptions = {}; +} + +HandlebarsTestBench.prototype.withInput = function(input) { + this.input = input; + return this; +}; + +HandlebarsTestBench.prototype.withHelper = function(name, helperFunction) { + this.helpers[name] = helperFunction; + return this; +}; + +HandlebarsTestBench.prototype.withPartial = function(name, partialAsString) { + this.partials[name] = partialAsString; + return this; +}; + +HandlebarsTestBench.prototype.withCompileOptions = function(compileOptions) { + this.compileOptions = compileOptions; + return this; +}; + +HandlebarsTestBench.prototype.withRuntimeOptions = function(runtimeOptions) { + this.runtimeOptions = runtimeOptions; + return this; +}; + +HandlebarsTestBench.prototype.withMessage = function(message) { + this.message = message; + return this; +}; + +HandlebarsTestBench.prototype.toCompileTo = function(expectedOutputAsString) { + var self = this; + var compile = Object.keys(this.partials).length > 0 + ? CompilerContext.compileWithPartial + : CompilerContext.compile; + + var combinedRuntimeOptions = {}; + Object.keys(this.runtimeOptions).forEach(function(key) { + combinedRuntimeOptions[key] = self.runtimeOptions[key]; + }); + combinedRuntimeOptions.helpers = this.helpers; + combinedRuntimeOptions.partials = this.partials; + + var template = compile(this.templateAsString, this.compileOptions); + var output = template(this.input, combinedRuntimeOptions); + + if (output !== expectedOutputAsString) { + // Error message formatted so that IntelliJ-Idea shows "diff"-button + // https://stackoverflow.com/a/10945655/4251384 + throw new AssertError(this.message + '\nexpected:' + expectedOutputAsString + 'but was:' + output); + } +}; diff --git a/spec/security.js b/spec/security.js index c50bda6d..80b8e23c 100644 --- a/spec/security.js +++ b/spec/security.js @@ -1,11 +1,26 @@ describe('security issues', function() { describe('GH-1495: Prevent Remote Code Execution via constructor', function() { it('should not allow constructors to be accessed', function() { - shouldCompileTo('{{constructor.name}}', {}, ''); - shouldCompileTo('{{lookup (lookup this "constructor") "name"}}', {}, ''); + expectTemplate('{{lookup (lookup this "constructor") "name"}}') + .withInput({}) + .toCompileTo(''); + + expectTemplate('{{constructor.name}}') + .withInput({}) + .toCompileTo(''); }); - it('should allow the "constructor" property to be accessed if it is enumerable', function() { + 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 enumerable', function() { shouldCompileTo('{{constructor.name}}', {'constructor': { 'name': 'here we go' }}, 'here we go'); @@ -14,6 +29,13 @@ describe('security issues', function() { }}, 'here we go'); }); + it('should allow the "constructor" property to be accessed if it is enumerable', function() { + shouldCompileTo('{{lookup (lookup this "constructor") "name"}}', {'constructor': { + 'name': 'here we go' + }}, 'here we go'); + }); + + it('should allow prototype properties that are not constructors', function() { function TestClass() { }