Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport security fixes for #1736 #1751

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 18 additions & 2 deletions lib/handlebars/runtime.js
Expand Up @@ -69,12 +69,28 @@ export function template(templateSpec, env) {
if (!(name in obj)) {
throw new Exception('"' + name + '" not defined in ' + obj);
}
return obj[name];
return container.lookupProperty(obj, name);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears strict mode is already covered by the check in javascript-compiler.js:

if (dangerousPropertyRegex.test(name)) {
const isOwnProperty = [ this.aliasable('Object.prototype.hasOwnProperty'), '.call(', parent, ',', JSON.stringify(name), ')'];
return ['(', isOwnProperty, '?', _actualLookup(), ' : undefined)'];
}

However, the reason why this vulnerability appeared in 4.x was because that check was removed and this change was present. Better to make this change to make it completely clear.

},
lookupProperty: function(parent, propertyName) {
let result = parent[propertyName];
if (result == null) {
return result;
}
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return result;
}

if (!Utils.dangerousPropertyRegex.test(String(propertyName))) {
return result;
}

return undefined;
},
lookup: function(depths, name) {
const len = depths.length;
for (let i = 0; i < len; i++) {
if (depths[i] && depths[i][name] != null) {
let result = depths[i] && container.lookupProperty(depths[i], name);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This covers compat mode.

if (result != null) {
return depths[i][name];
}
}
Expand Down
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -36,6 +36,9 @@
"babel-loader": "^5.0.0",
"babel-runtime": "^5.1.10",
"benchmark": "~1.0",
"chai": "^4.2.0",
"chai-diff": "^1.0.1",
"dirty-chai": "^2.0.1",
"dustjs-linkedin": "^2.0.2",
"eco": "~1.1.0-rc-3",
"grunt": "~0.4.1",
Expand Down
9 changes: 5 additions & 4 deletions spec/.eslintrc
Expand Up @@ -3,12 +3,11 @@
"CompilerContext": true,
"Handlebars": true,
"handlebarsEnv": true,

"shouldCompileTo": true,
"shouldCompileToWithPartials": true,
"shouldThrow": true,
"expectTemplate": true,
"compileWithPartials": true,

"console": true,
"require": true,
"suite": true,
Expand All @@ -22,15 +21,17 @@
"stop": true,
"ok": true,
"strictEqual": true,
"define": true
"define": true,
"expect": true,
"chai": true
},
"env": {
"mocha": true
},
"rules": {
// Disabling for tests, for now.
"no-path-concat": 0,

"dot-notation": 0,
"no-var": 0
}
}
118 changes: 118 additions & 0 deletions spec/env/common.js
Expand Up @@ -72,3 +72,121 @@ global.shouldThrow = function(callback, type, msg) {
throw new Error('It failed to throw');
}
};


global.expectTemplate = function(templateAsString) {
return new HandlebarsTestBench(templateAsString);
};

function HandlebarsTestBench(templateAsString) {
this.templateAsString = templateAsString;
this.helpers = {};
this.partials = {};
this.decorators = {};
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.withHelpers = function(helperFunctions) {
var self = this;
Object.keys(helperFunctions).forEach(function(name) {
self.withHelper(name, helperFunctions[name]);
});
return this;
};

HandlebarsTestBench.prototype.withPartial = function(name, partialAsString) {
this.partials[name] = partialAsString;
return this;
};

HandlebarsTestBench.prototype.withPartials = function(partials) {
var self = this;
Object.keys(partials).forEach(function(name) {
self.withPartial(name, partials[name]);
});
return this;
};

HandlebarsTestBench.prototype.withDecorator = function(
name,
decoratorFunction
) {
this.decorators[name] = decoratorFunction;
return this;
};

HandlebarsTestBench.prototype.withDecorators = function(decorators) {
var self = this;
Object.keys(decorators).forEach(function(name) {
self.withDecorator(name, decorators[name]);
});
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) {
expect(this._compileAndExecute()).to.equal(
expectedOutputAsString,
this.message
);
};

// see chai "to.throw" (https://www.chaijs.com/api/bdd/#method_throw)
HandlebarsTestBench.prototype.toThrow = function(errorLike, errMsgMatcher) {
var self = this;
expect(function() {
self._compileAndExecute();
}).to.throw(errorLike, errMsgMatcher, this.message);
};

HandlebarsTestBench.prototype._compileAndExecute = function() {
var compile =
Object.keys(this.partials).length > 0
? CompilerContext.compileWithPartial
: CompilerContext.compile;

var combinedRuntimeOptions = this._combineRuntimeOptions();

var template = compile(this.templateAsString, this.compileOptions);
return template(this.input, combinedRuntimeOptions);
};

HandlebarsTestBench.prototype._combineRuntimeOptions = function() {
var self = this;
var combinedRuntimeOptions = {};
Object.keys(this.runtimeOptions).forEach(function(key) {
combinedRuntimeOptions[key] = self.runtimeOptions[key];
});
combinedRuntimeOptions.helpers = this.helpers;
combinedRuntimeOptions.partials = this.partials;
combinedRuntimeOptions.decorators = this.decorators;
return combinedRuntimeOptions;
};
6 changes: 6 additions & 0 deletions spec/env/node.js
@@ -1,5 +1,11 @@
require('./common');

var chai = require('chai');
var dirtyChai = require('dirty-chai');

chai.use(dirtyChai);
global.expect = chai.expect;

global.Handlebars = require('../../lib');

global.CompilerContext = {
Expand Down
85 changes: 71 additions & 14 deletions spec/security.js
@@ -1,17 +1,30 @@
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"}}', {}, '');
});
checkPropertyAccess({});

describe('in compat-mode', function() {
checkPropertyAccess({ compat: true });
});

describe('in strict-mode', function() {
checkPropertyAccess({ strict: true });
});


function checkPropertyAccess(compileOptions) {
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');
shouldCompileTo('{{lookup (lookup this "constructor") "name"}}', {'constructor': {
'name': 'here we go'
}}, 'here we go');
expectTemplate('{{constructor.name}}')
.withCompileOptions(compileOptions)
.withInput({'constructor': {
'name': 'here we go'
}})
.toCompileTo('here we go');
expectTemplate('{{lookup (lookup this "constructor") "name"}}')
.withCompileOptions(compileOptions)
.withInput({'constructor': {
'name': 'here we go'
}})
.toCompileTo('here we go');
});

it('should allow prototype properties that are not constructors', function() {
Expand All @@ -24,11 +37,55 @@ describe('security issues', function() {
}
});

shouldCompileTo('{{#with this}}{{this.abc}}{{/with}}',
new TestClass(), 'xyz');
shouldCompileTo('{{#with this}}{{lookup this "abc"}}{{/with}}',
new TestClass(), 'xyz');

expectTemplate('{{#with this}}{{this.abc}}{{/with}}')
.withCompileOptions(compileOptions)
.withInput(new TestClass())
.toCompileTo('xyz');

expectTemplate('{{#with this}}{{lookup this "abc"}}{{/with}}')
.withCompileOptions(compileOptions)
.withInput(new TestClass())
.toCompileTo('xyz');
});

it('should not allow constructors to be accessed', function() {
expectTemplate('{{lookup (lookup this "constructor") "name"}}')
.withCompileOptions(compileOptions)
.withInput({})
.toCompileTo('');
if (compileOptions.strict) {
expectTemplate('{{constructor.name}}')
.withCompileOptions(compileOptions)
.withInput({})
.toThrow(TypeError);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was already the case in 3.x before this change and remains after this change currently. strict mode resulted in a TypeError when accessing constructor, __proto__, etc. We can fix this but it would require more changes and would not improve the security.

} else {
expectTemplate('{{constructor.name}}')
.withCompileOptions(compileOptions)
.withInput({})
.toCompileTo('');
}
});

it('should not allow __proto__ to be accessed', function() {
expectTemplate('{{lookup (lookup this "__proto__") "name"}}')
.withCompileOptions(compileOptions)
.withInput({})
.toCompileTo('');
if (compileOptions.strict) {
expectTemplate('{{__proto__.name}}')
.withCompileOptions(compileOptions)
.withInput({})
.toThrow(TypeError);
} else {
expectTemplate('{{__proto__.name}}')
.withCompileOptions(compileOptions)
.withInput({})
.toCompileTo('');
}
});

}
});

describe('GH-1595', function() {
Expand Down