Merge branch '4.x'

# Conflicts:
#	components/bower.json
#	components/handlebars.js.nuspec
#	components/package.json
#	package.json
This commit is contained in:
Nils Knappmeier
2020-01-13 21:58:50 +01:00
11 changed files with 367 additions and 134 deletions
+1 -1
View File
@@ -215,7 +215,7 @@ module.exports = function(grunt) {
grunt.registerTask('bench', ['metrics']);
if (process.env.SAUCE_USERNAME) {
if (process.env.SAUCE_ACCESS_KEY) {
grunt.registerTask('sauce', ['concat:tests', 'connect', 'saucelabs-mocha']);
} else {
grunt.registerTask('sauce', []);
+9 -1
View File
@@ -3,8 +3,9 @@ import Exception from './exception';
import { registerDefaultHelpers } from './helpers';
import { registerDefaultDecorators } from './decorators';
import logger from './logger';
import { resetLoggedProperties } from './internal/proto-access';
export const VERSION = '4.6.0';
export const VERSION = '4.7.2';
export const COMPILER_REVISION = 8;
export const LAST_COMPATIBLE_COMPILER_REVISION = 7;
@@ -78,6 +79,13 @@ HandlebarsEnvironment.prototype = {
},
unregisterDecorator: function(name) {
delete this.decorators[name];
},
/**
* Reset the memory of illegal property accesses that have already been logged.
* @deprecated should only be used in handlebars test-cases
*/
resetLoggedPropertyAccesses() {
resetLoggedProperties();
}
};
+70
View File
@@ -0,0 +1,70 @@
import { createNewLookupObject } from './create-new-lookup-object';
import * as logger from '../logger';
const loggedProperties = Object.create(null);
export function createProtoAccessControl(runtimeOptions) {
let defaultMethodWhiteList = Object.create(null);
defaultMethodWhiteList['constructor'] = false;
defaultMethodWhiteList['__defineGetter__'] = false;
defaultMethodWhiteList['__defineSetter__'] = false;
defaultMethodWhiteList['__lookupGetter__'] = false;
let defaultPropertyWhiteList = Object.create(null);
// eslint-disable-next-line no-proto
defaultPropertyWhiteList['__proto__'] = false;
return {
properties: {
whitelist: createNewLookupObject(
defaultPropertyWhiteList,
runtimeOptions.allowedProtoProperties
),
defaultValue: runtimeOptions.allowProtoPropertiesByDefault
},
methods: {
whitelist: createNewLookupObject(
defaultMethodWhiteList,
runtimeOptions.allowedProtoMethods
),
defaultValue: runtimeOptions.allowProtoMethodsByDefault
}
};
}
export function resultIsAllowed(result, protoAccessControl, propertyName) {
if (typeof result === 'function') {
return checkWhiteList(protoAccessControl.methods, propertyName);
} else {
return checkWhiteList(protoAccessControl.properties, propertyName);
}
}
function checkWhiteList(protoAccessControlForType, propertyName) {
if (protoAccessControlForType.whitelist[propertyName] !== undefined) {
return protoAccessControlForType.whitelist[propertyName] === true;
}
if (protoAccessControlForType.defaultValue !== undefined) {
return protoAccessControlForType.defaultValue;
}
logUnexpecedPropertyAccessOnce(propertyName);
return false;
}
function logUnexpecedPropertyAccessOnce(propertyName) {
if (loggedProperties[propertyName] !== true) {
loggedProperties[propertyName] = true;
logger.log(
'error',
`Handlebars: Access has been denied to resolve the property "${propertyName}" because it is not an "own property" of its parent.\n` +
`You can add a runtime option to disable the check or this warning:\n` +
`See https://handlebarsjs.com/api-reference/runtime-options.html#options-to-control-prototype-access for details`
);
}
}
export function resetLoggedProperties() {
Object.keys(loggedProperties).forEach(propertyName => {
delete loggedProperties[propertyName];
});
}
+5
View File
@@ -1,4 +1,9 @@
export function wrapHelper(helper, transformOptionsFn) {
if (typeof helper !== 'function') {
// This should not happen, but apparently it does in https://github.com/wycats/handlebars.js/issues/1639
// We try to make the wrapper least-invasive by not wrapping it, if the helper is not a function.
return helper;
}
let wrapper = function(/* dynamic arguments */) {
const options = arguments[arguments.length - 1];
arguments[arguments.length - 1] = transformOptionsFn(options);
+13 -16
View File
@@ -8,7 +8,10 @@ import {
} from './base';
import { moveHelperToHooks } from './helpers';
import { wrapHelper } from './internal/wrapHelper';
import { createNewLookupObject } from './internal/createNewLookupObject';
import {
createProtoAccessControl,
resultIsAllowed
} from './internal/proto-access';
export function checkRevision(compilerInfo) {
const compilerRevision = (compilerInfo && compilerInfo[0]) || 1,
@@ -70,8 +73,7 @@ export function template(templateSpec, env) {
let extendedOptions = Utils.extend({}, options, {
hooks: this.hooks,
allowedProtoMethods: this.allowedProtoMethods,
allowedProtoProperties: this.allowedProtoProperties
protoAccessControl: this.protoAccessControl
});
let result = env.VM.invokePartial.call(
@@ -123,15 +125,14 @@ export function template(templateSpec, env) {
},
lookupProperty: function(parent, propertyName) {
let result = parent[propertyName];
if (result == null) {
return result;
}
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return result;
}
const whitelist =
typeof result === 'function'
? container.allowedProtoMethods
: container.allowedProtoProperties;
if (whitelist[propertyName] === true) {
if (resultIsAllowed(result, container.protoAccessControl, propertyName)) {
return result;
}
return undefined;
@@ -234,6 +235,7 @@ export function template(templateSpec, env) {
)
);
}
main = executeDecorators(
templateSpec.main,
main,
@@ -244,6 +246,7 @@ export function template(templateSpec, env) {
);
return main(context, options);
}
ret.isTop = true;
function _setup(options) {
@@ -268,12 +271,7 @@ export function template(templateSpec, env) {
}
container.hooks = {};
container.allowedProtoProperties = createNewLookupObject(
options.allowedProtoProperties
);
container.allowedProtoMethods = createNewLookupObject(
options.allowedProtoMethods
);
container.protoAccessControl = createProtoAccessControl(options);
let keepHelperInHelpers =
options.allowCallsToHelperMissing ||
@@ -281,8 +279,7 @@ export function template(templateSpec, env) {
moveHelperToHooks(container, 'helperMissing', keepHelperInHelpers);
moveHelperToHooks(container, 'blockHelperMissing', keepHelperInHelpers);
} else {
container.allowedProtoProperties = options.allowedProtoProperties;
container.allowedProtoMethods = options.allowedProtoMethods;
container.protoAccessControl = options.protoAccessControl; // internal option
container.helpers = options.helpers;
container.partials = options.partials;
container.decorators = options.decorators;
+45 -1
View File
@@ -2,7 +2,51 @@
## Development
[Commits](https://github.com/wycats/handlebars.js/compare/v4.6.0...master)
[Commits](https://github.com/wycats/handlebars.js/compare/v4.7.2...master)
## v4.7.2 - January 13th, 2020
Bugfixes:
- fix: don't wrap helpers that are not functions - 9d5aa36, #1639
Chore/Build:
- chore: execute saucelabs-task only if access-key exists - a4fd391
Compatibility notes:
- No breaking changes are to be expected
[Commits](https://github.com/wycats/handlebars.js/compare/v4.7.1...v4.7.2)
## v4.7.1 - January 12th, 2020
Bugfixes:
- fix: fix log output in case of illegal property access - f152dfc
- fix: log error for illegal property access only once per property - 3c1e252
Compatibility notes:
- no incompatibilities are to be expected.
[Commits](https://github.com/wycats/handlebars.js/compare/v4.7.0...v4.7.1)
## v4.7.0 - January 10th, 2020
Features:
- feat: default options for controlling proto access - 7af1c12, #1635
- This makes it possible to disable the prototype access restrictions added in 4.6.0
- an error is logged in the console, if access to prototype properties is attempted and denied
and no explicit configuration has taken place.
Compatibility notes:
- no compatibilities are expected
[Commits](https://github.com/wycats/handlebars.js/compare/v4.6.0...v4.7.0)
## v4.6.0 - January 8th, 2020
+9
View File
@@ -529,4 +529,13 @@ describe('Regressions', function() {
sinon.restore();
});
});
describe("GH-1639: TypeError: Cannot read property 'apply' of undefined\" when handlebars version > 4.6.0 (undocumented, deprecated usage)", function() {
it('should treat undefined helpers like non-existing helpers', function() {
expectTemplate('{{foo}}')
.withHelper('foo', undefined)
.withInput({ foo: 'bar' })
.toCompileTo('bar');
});
});
});
+207 -111
View File
@@ -149,176 +149,272 @@ describe('security issues', function() {
});
});
describe('GH-1595', function() {
it('properties, that are required to be own properties', function() {
expectTemplate('{{constructor}}')
.withInput({})
.toCompileTo('');
describe('GH-1595: dangerous properties', function() {
var templates = [
'{{constructor}}',
'{{__defineGetter__}}',
'{{__defineSetter__}}',
'{{__lookupGetter__}}',
'{{__proto__}}',
'{{lookup this "constructor"}}',
'{{lookup this "__defineGetter__"}}',
'{{lookup this "__defineSetter__"}}',
'{{lookup this "__lookupGetter__"}}',
'{{lookup this "__proto__"}}'
];
expectTemplate('{{__defineGetter__}}')
.withInput({})
.toCompileTo('');
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() {}
expectTemplate('{{__defineSetter__}}')
.withInput({})
.toCompileTo('');
TestClass.prototype.aProperty = 'propertyValue';
TestClass.prototype.aMethod = function() {
return 'returnValue';
};
expectTemplate('{{__lookupGetter__}}')
.withInput({})
.toCompileTo('');
expectTemplate('{{__proto__}}')
.withInput({})
.toCompileTo('');
expectTemplate('{{lookup this "constructor"}}')
.withInput({})
.toCompileTo('');
expectTemplate('{{lookup this "__defineGetter__"}}')
.withInput({})
.toCompileTo('');
expectTemplate('{{lookup this "__defineSetter__"}}')
.withInput({})
.toCompileTo('');
expectTemplate('{{lookup this "__lookupGetter__"}}')
.withInput({})
.toCompileTo('');
expectTemplate('{{lookup this "__proto__"}}')
.withInput({})
.toCompileTo('');
beforeEach(function() {
handlebarsEnv.resetLoggedPropertyAccesses();
});
describe('GH-1631: disallow access to prototype functions', function() {
function TestClass() {}
afterEach(function() {
sinon.restore();
});
TestClass.prototype.aProperty = 'propertyValue';
TestClass.prototype.aMethod = function() {
return 'returnValue';
};
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');
describe('control access to prototype methods via "allowedProtoMethods"', function() {
it('should be prohibited by default', function() {
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', function() {
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('should be prohibited by default (in "compat" mode)', function() {
expectTemplate('{{aMethod}}')
.withInput(new TestClass())
.withCompileOptions({ compat: true })
.toCompileTo('');
});
it('can be turned on by default, which disables the warning', function() {
var spy = sinon.spy(console, 'error');
it('can be allowed (in "compat" mode)', function() {
expectTemplate('{{aMethod}}')
.withInput(new TestClass())
.withCompileOptions({ compat: true })
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowedProtoMethods: {
aMethod: true
}
allowProtoMethodsByDefault: true
})
.toCompileTo('returnValue');
expect(spy.callCount).to.equal(0);
});
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('can be turned off by default, which disables the warning', function() {
var spy = sinon.spy(console, 'error');
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 })
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: {
trim: true
aMethod: false
}
})
.toCompileTo('abc');
});
});
describe('control access to prototype non-methods via "allowedProtoProperties"', function() {
it('should be prohibited by default', function() {
expectTemplate('{{aProperty}}')
.withInput(new TestClass())
.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 });
});
function checkProtoPropertyAccess(compileOptions) {
it('should be prohibited by default and log a warning', function() {
var spy = sinon.spy(console, 'error');
it('can be turned on', function() {
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('should be prohibited by default (in "compat" mode)', function() {
expectTemplate('{{aProperty}}')
.withInput(new TestClass())
.withCompileOptions({ compat: true })
.toCompileTo('');
});
it('can be turned on by default, which disables the warning', function() {
var spy = sinon.spy(console, 'error');
it('can be turned on (in "compat" mode)', function() {
expectTemplate('{{aProperty}}')
.withInput(new TestClass())
.withCompileOptions({ compat: true })
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowedProtoProperties: {
aProperty: true
}
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);
});
});
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();
});
afterEach(function() {
sinon.restore();
});
it('should work with simple properties', function() {
expectTemplate('{{aProperty}}')
.withInput({ aProperty: 'propertyValue' })
.toCompileTo('propertyValue');
});
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');
});
it('should work with Array.prototype.length', function() {
expectTemplate('{{anArray.length}}')
.withInput({ anArray: ['a', 'b', 'c'] })
.toCompileTo('3');
});
});
});
+4 -2
View File
@@ -30,8 +30,10 @@ declare namespace Handlebars {
data?: any;
blockParams?: any[];
allowCallsToHelperMissing?: boolean;
allowedProtoProperties?: { [name: string]: boolean }
allowedProtoMethods?: { [name: string]: boolean }
allowedProtoProperties?: { [name: string]: boolean };
allowedProtoMethods?: { [name: string]: boolean };
allowProtoPropertiesByDefault?: boolean;
allowProtoMethodsByDefault?: boolean;
}
export interface HelperOptions {
+4 -2
View File
@@ -241,12 +241,14 @@ function testExceptionWithNodeTypings() {
let stack: string | undefined = exception.stack;
}
function testProtoPropertyControlOptions() {
function testProtoAccessControlControlOptions() {
Handlebars.compile('test')(
{},
{
allowedProtoMethods: { allowedMethod: true, forbiddenMethod: false },
allowedProtoProperties: { allowedProperty: true, forbiddenProperty: false }
allowedProtoProperties: { allowedProperty: true, forbiddenProperty: false },
allowProtoMethodsByDefault: true,
allowProtoPropertiesByDefault: false,
}
);
}