68d8df5a88
Fixes GHSA-2w6w-674q-4c4q, GHSA-xhpv-hc6g-r9c6, GHSA-3mfm-83xf-c92r, GHSA-2qvq-rjwj-gvw9, GHSA-9cx6-37pm-9jff, GHSA-7rx3-28cr-v5wh, GHSA-442j-39wm-28r2, GHSA-xjpj-3mr7-gcpf
588 lines
19 KiB
JavaScript
588 lines
19 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() {
|
|
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 = '<img src=x onerror="alert(1)">';
|
|
|
|
expect(function() {
|
|
Handlebars.compile('<div>{{> widget}}</div>')({});
|
|
}).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'] = '<img src=x onerror="alert(1)">';
|
|
|
|
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'] = '<img src=x onerror="alert(1)">';
|
|
|
|
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', '<div>{{> @partial-block}}</div>');
|
|
try {
|
|
var result = Handlebars.compile('{{#> wrapper}}hello{{/wrapper}}')({});
|
|
expect(result).to.equal('<div>hello</div>');
|
|
} finally {
|
|
Handlebars.unregisterPartial('wrapper');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
function wrapToAdjustContainer(precompiledTemplateFunction) {
|
|
return function templateFunctionWrapper(container /*, more args */) {
|
|
delete container.lookupProperty;
|
|
return precompiledTemplateFunction.apply(this, arguments);
|
|
};
|
|
}
|