Files
handlebars.js/spec/security.js
T
Igor Savin 169ef75066 Remove deprecated helpers and fix release script (#2128)
* Remove deprecated helpers
* Use more idiomatic assertions
* Fix release script
2026-03-03 21:52:28 +01:00

458 lines
15 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');
},
});
}).toThrow();
expect(functionCalls.length).toBe(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 }
);
expect(functionCalls.length).toBe(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 () {
var browserSupportsExploit =
{}.__defineGetter__ != null && {}.__lookupGetter__ != null;
it.skipIf(!browserSupportsExploit)(
'should not allow to access constructor after overriding via __defineGetter__',
function () {
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';
};
beforeEach(function () {
handlebarsEnv.resetLoggedPropertyAccesses();
});
afterEach(function () {
vi.restoreAllMocks();
});
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 = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.toCompileTo('');
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toMatch(
/Handlebars: Access has been denied/
);
});
it('should only log the warning once', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.toCompileTo('');
expectTemplate('{{aMethod}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.toCompileTo('');
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toMatch(
/Handlebars: Access has been denied/
);
});
it('can be allowed, which disables the warning', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowedProtoMethods: {
aMethod: true,
},
})
.toCompileTo('returnValue');
expect(spy).not.toHaveBeenCalled();
});
it('can be turned on by default, which disables the warning', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowProtoMethodsByDefault: true,
})
.toCompileTo('returnValue');
expect(spy).not.toHaveBeenCalled();
});
it('can be turned off by default, which disables the warning', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowProtoMethodsByDefault: false,
})
.toCompileTo('');
expect(spy).not.toHaveBeenCalled();
});
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 = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.toCompileTo('');
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toMatch(
/Handlebars: Access has been denied/
);
});
it('can be explicitly prohibited by default, which disables the warning', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowProtoPropertiesByDefault: false,
})
.toCompileTo('');
expect(spy).not.toHaveBeenCalled();
});
it('can be turned on, which disables the warning', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowedProtoProperties: {
aProperty: true,
},
})
.toCompileTo('propertyValue');
expect(spy).not.toHaveBeenCalled();
});
it('can be turned on by default, which disables the warning', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowProtoPropertiesByDefault: true,
})
.toCompileTo('propertyValue');
expect(spy).not.toHaveBeenCalled();
});
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;
vi.spyOn(handlebarsEnv, 'template').mockImplementation(
function (templateSpec) {
templateSpec.main = wrapToAdjustContainer(templateSpec.main);
return oldTemplateMethod.call(this, templateSpec);
}
);
});
afterEach(function () {
vi.restoreAllMocks();
});
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');
});
});
});
function wrapToAdjustContainer(precompiledTemplateFunction) {
return function templateFunctionWrapper(container /*, more args */) {
delete container.lookupProperty;
return precompiledTemplateFunction.apply(this, arguments);
};
}