7af1c12db6
This commmit adds the runtime options - `allowProtoPropertiesByDefault` (boolean, default: false) and - `allowProtoMethodsByDefault` (boolean, default: false)` which can be used to allow access to prototype properties and functions in general. Specific properties and methods can still be disabled from access via `allowedProtoProperties` and `allowedProtoMethods` by setting the corresponding values to false. The methods `constructor`, `__defineGetter__`, `__defineSetter__`, `__lookupGetter__` and the property `__proto__` will be disabled, even if the allow...ByDefault-options are set to true. In order to allow access to those properties and methods, they have to be explicitly set to true in the 'allowedProto...'-options. A warning is logged when the a proto-access it attempted and denied by default (i.e. if no option is set by the user to make the access decision explicit)
408 lines
13 KiB
JavaScript
408 lines
13 KiB
JavaScript
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() {
|
|
shouldCompileTo(
|
|
'{{constructor.name}}',
|
|
{
|
|
constructor: {
|
|
name: 'here we go'
|
|
}
|
|
},
|
|
'here we go'
|
|
);
|
|
shouldCompileTo(
|
|
'{{lookup (lookup this "constructor") "name"}}',
|
|
{
|
|
constructor: {
|
|
name: 'here we go'
|
|
}
|
|
},
|
|
'here we go'
|
|
);
|
|
});
|
|
|
|
it('should allow the "constructor" property to be accessed if it is an "own property"', function() {
|
|
shouldCompileTo(
|
|
'{{lookup (lookup this "constructor") "name"}}',
|
|
{
|
|
constructor: {
|
|
name: 'here we go'
|
|
}
|
|
},
|
|
'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() {
|
|
shouldThrow(function() {
|
|
var template = Handlebars.compile('{{helperMissing}}');
|
|
template({});
|
|
}, Error);
|
|
});
|
|
it('should throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', function() {
|
|
shouldThrow(function() {
|
|
var template = Handlebars.compile(
|
|
'{{#helperMissing}}{{/helperMissing}}'
|
|
);
|
|
template({});
|
|
}, 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() {
|
|
shouldThrow(function() {
|
|
var template = Handlebars.compile(
|
|
'{{#blockHelperMissing .}}{{/blockHelperMissing}}'
|
|
);
|
|
template({
|
|
fn: function() {
|
|
return 'functionInData';
|
|
}
|
|
});
|
|
}, 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__}}',
|
|
'{{__proto__}}',
|
|
'{{lookup this "constructor"}}',
|
|
'{{lookup this "__defineGetter__"}}',
|
|
'{{lookup this "__defineSetter__"}}',
|
|
'{{lookup this "__lookupGetter__"}}',
|
|
'{{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';
|
|
};
|
|
|
|
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('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 });
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
function wrapToAdjustContainer(precompiledTemplateFunction) {
|
|
return function templateFunctionWrapper(container /*, more args */) {
|
|
delete container.lookupProperty;
|
|
return precompiledTemplateFunction.apply(this, arguments);
|
|
};
|
|
}
|