Skip to content

Commit e61e016

Browse files
committed
fix: check dangerous property access in strict-mode (handlebars-lang#1736)
1 parent 16bd606 commit e61e016

File tree

6 files changed

+221
-20
lines changed

6 files changed

+221
-20
lines changed

lib/handlebars/runtime.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,28 @@ export function template(templateSpec, env) {
6969
if (!(name in obj)) {
7070
throw new Exception('"' + name + '" not defined in ' + obj);
7171
}
72-
return obj[name];
72+
return container.lookupProperty(obj, name);
73+
},
74+
lookupProperty: function(parent, propertyName) {
75+
let result = parent[propertyName];
76+
if (result == null) {
77+
return result;
78+
}
79+
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
80+
return result;
81+
}
82+
83+
if (!Utils.dangerousPropertyRegex.test(String(propertyName))) {
84+
return result;
85+
}
86+
87+
return undefined;
7388
},
7489
lookup: function(depths, name) {
7590
const len = depths.length;
7691
for (let i = 0; i < len; i++) {
77-
if (depths[i] && depths[i][name] != null) {
92+
let result = depths[i] && container.lookupProperty(depths[i], name);
93+
if (result != null) {
7894
return depths[i][name];
7995
}
8096
}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
"babel-loader": "^5.0.0",
3737
"babel-runtime": "^5.1.10",
3838
"benchmark": "~1.0",
39+
"chai": "^4.2.0",
40+
"chai-diff": "^1.0.1",
41+
"dirty-chai": "^2.0.1",
3942
"dustjs-linkedin": "^2.0.2",
4043
"eco": "~1.1.0-rc-3",
4144
"grunt": "~0.4.1",

spec/.eslintrc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
"CompilerContext": true,
44
"Handlebars": true,
55
"handlebarsEnv": true,
6-
76
"shouldCompileTo": true,
87
"shouldCompileToWithPartials": true,
98
"shouldThrow": true,
9+
"expectTemplate": true,
1010
"compileWithPartials": true,
11-
1211
"console": true,
1312
"require": true,
1413
"suite": true,
@@ -22,15 +21,17 @@
2221
"stop": true,
2322
"ok": true,
2423
"strictEqual": true,
25-
"define": true
24+
"define": true,
25+
"expect": true,
26+
"chai": true
2627
},
2728
"env": {
2829
"mocha": true
2930
},
3031
"rules": {
3132
// Disabling for tests, for now.
3233
"no-path-concat": 0,
33-
34+
"dot-notation": 0,
3435
"no-var": 0
3536
}
3637
}

spec/env/common.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,121 @@ global.shouldThrow = function(callback, type, msg) {
7272
throw new Error('It failed to throw');
7373
}
7474
};
75+
76+
77+
global.expectTemplate = function(templateAsString) {
78+
return new HandlebarsTestBench(templateAsString);
79+
};
80+
81+
function HandlebarsTestBench(templateAsString) {
82+
this.templateAsString = templateAsString;
83+
this.helpers = {};
84+
this.partials = {};
85+
this.decorators = {};
86+
this.input = {};
87+
this.message =
88+
'Template' + templateAsString + ' does not evaluate to expected output';
89+
this.compileOptions = {};
90+
this.runtimeOptions = {};
91+
}
92+
93+
HandlebarsTestBench.prototype.withInput = function(input) {
94+
this.input = input;
95+
return this;
96+
};
97+
98+
HandlebarsTestBench.prototype.withHelper = function(name, helperFunction) {
99+
this.helpers[name] = helperFunction;
100+
return this;
101+
};
102+
103+
HandlebarsTestBench.prototype.withHelpers = function(helperFunctions) {
104+
var self = this;
105+
Object.keys(helperFunctions).forEach(function(name) {
106+
self.withHelper(name, helperFunctions[name]);
107+
});
108+
return this;
109+
};
110+
111+
HandlebarsTestBench.prototype.withPartial = function(name, partialAsString) {
112+
this.partials[name] = partialAsString;
113+
return this;
114+
};
115+
116+
HandlebarsTestBench.prototype.withPartials = function(partials) {
117+
var self = this;
118+
Object.keys(partials).forEach(function(name) {
119+
self.withPartial(name, partials[name]);
120+
});
121+
return this;
122+
};
123+
124+
HandlebarsTestBench.prototype.withDecorator = function(
125+
name,
126+
decoratorFunction
127+
) {
128+
this.decorators[name] = decoratorFunction;
129+
return this;
130+
};
131+
132+
HandlebarsTestBench.prototype.withDecorators = function(decorators) {
133+
var self = this;
134+
Object.keys(decorators).forEach(function(name) {
135+
self.withDecorator(name, decorators[name]);
136+
});
137+
return this;
138+
};
139+
140+
HandlebarsTestBench.prototype.withCompileOptions = function(compileOptions) {
141+
this.compileOptions = compileOptions;
142+
return this;
143+
};
144+
145+
HandlebarsTestBench.prototype.withRuntimeOptions = function(runtimeOptions) {
146+
this.runtimeOptions = runtimeOptions;
147+
return this;
148+
};
149+
150+
HandlebarsTestBench.prototype.withMessage = function(message) {
151+
this.message = message;
152+
return this;
153+
};
154+
155+
HandlebarsTestBench.prototype.toCompileTo = function(expectedOutputAsString) {
156+
expect(this._compileAndExecute()).to.equal(
157+
expectedOutputAsString,
158+
this.message
159+
);
160+
};
161+
162+
// see chai "to.throw" (https://www.chaijs.com/api/bdd/#method_throw)
163+
HandlebarsTestBench.prototype.toThrow = function(errorLike, errMsgMatcher) {
164+
var self = this;
165+
expect(function() {
166+
self._compileAndExecute();
167+
}).to.throw(errorLike, errMsgMatcher, this.message);
168+
};
169+
170+
HandlebarsTestBench.prototype._compileAndExecute = function() {
171+
var compile =
172+
Object.keys(this.partials).length > 0
173+
? CompilerContext.compileWithPartial
174+
: CompilerContext.compile;
175+
176+
var combinedRuntimeOptions = this._combineRuntimeOptions();
177+
178+
var template = compile(this.templateAsString, this.compileOptions);
179+
return template(this.input, combinedRuntimeOptions);
180+
};
181+
182+
HandlebarsTestBench.prototype._combineRuntimeOptions = function() {
183+
var self = this;
184+
var combinedRuntimeOptions = {};
185+
Object.keys(this.runtimeOptions).forEach(function(key) {
186+
combinedRuntimeOptions[key] = self.runtimeOptions[key];
187+
});
188+
combinedRuntimeOptions.helpers = this.helpers;
189+
combinedRuntimeOptions.partials = this.partials;
190+
combinedRuntimeOptions.decorators = this.decorators;
191+
return combinedRuntimeOptions;
192+
};

spec/env/node.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
require('./common');
22

3+
var chai = require('chai');
4+
var dirtyChai = require('dirty-chai');
5+
6+
chai.use(dirtyChai);
7+
global.expect = chai.expect;
8+
39
global.Handlebars = require('../../lib');
410

511
global.CompilerContext = {

spec/security.js

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,30 @@
11
describe('security issues', function() {
22
describe('GH-1495: Prevent Remote Code Execution via constructor', function() {
3-
it('should not allow constructors to be accessed', function() {
4-
shouldCompileTo('{{constructor.name}}', {}, '');
5-
shouldCompileTo('{{lookup (lookup this "constructor") "name"}}', {}, '');
6-
});
3+
checkPropertyAccess({});
4+
5+
describe('in compat-mode', function() {
6+
checkPropertyAccess({ compat: true });
7+
});
78

9+
describe('in strict-mode', function() {
10+
checkPropertyAccess({ strict: true });
11+
});
12+
13+
14+
function checkPropertyAccess(compileOptions) {
815
it('should allow the "constructor" property to be accessed if it is enumerable', function() {
9-
shouldCompileTo('{{constructor.name}}', {'constructor': {
10-
'name': 'here we go'
11-
}}, 'here we go');
12-
shouldCompileTo('{{lookup (lookup this "constructor") "name"}}', {'constructor': {
13-
'name': 'here we go'
14-
}}, 'here we go');
16+
expectTemplate('{{constructor.name}}')
17+
.withCompileOptions(compileOptions)
18+
.withInput({'constructor': {
19+
'name': 'here we go'
20+
}})
21+
.toCompileTo('here we go');
22+
expectTemplate('{{lookup (lookup this "constructor") "name"}}')
23+
.withCompileOptions(compileOptions)
24+
.withInput({'constructor': {
25+
'name': 'here we go'
26+
}})
27+
.toCompileTo('here we go');
1528
});
1629

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

27-
shouldCompileTo('{{#with this}}{{this.abc}}{{/with}}',
28-
new TestClass(), 'xyz');
29-
shouldCompileTo('{{#with this}}{{lookup this "abc"}}{{/with}}',
30-
new TestClass(), 'xyz');
40+
41+
expectTemplate('{{#with this}}{{this.abc}}{{/with}}')
42+
.withCompileOptions(compileOptions)
43+
.withInput(new TestClass())
44+
.toCompileTo('xyz');
45+
46+
expectTemplate('{{#with this}}{{lookup this "abc"}}{{/with}}')
47+
.withCompileOptions(compileOptions)
48+
.withInput(new TestClass())
49+
.toCompileTo('xyz');
50+
});
51+
52+
it('should not allow constructors to be accessed', function() {
53+
expectTemplate('{{lookup (lookup this "constructor") "name"}}')
54+
.withCompileOptions(compileOptions)
55+
.withInput({})
56+
.toCompileTo('');
57+
if (compileOptions.strict) {
58+
expectTemplate('{{constructor.name}}')
59+
.withCompileOptions(compileOptions)
60+
.withInput({})
61+
.toThrow(TypeError);
62+
} else {
63+
expectTemplate('{{constructor.name}}')
64+
.withCompileOptions(compileOptions)
65+
.withInput({})
66+
.toCompileTo('');
67+
}
3168
});
69+
70+
it('should not allow __proto__ to be accessed', function() {
71+
expectTemplate('{{lookup (lookup this "__proto__") "name"}}')
72+
.withCompileOptions(compileOptions)
73+
.withInput({})
74+
.toCompileTo('');
75+
if (compileOptions.strict) {
76+
expectTemplate('{{__proto__.name}}')
77+
.withCompileOptions(compileOptions)
78+
.withInput({})
79+
.toThrow(TypeError);
80+
} else {
81+
expectTemplate('{{__proto__.name}}')
82+
.withCompileOptions(compileOptions)
83+
.withInput({})
84+
.toCompileTo('');
85+
}
86+
});
87+
88+
}
3289
});
3390

3491
describe('GH-1595', function() {

0 commit comments

Comments
 (0)