Skip to content

Commit 97f7ad9

Browse files
committed
1 parent c0bf543 commit 97f7ad9

File tree

3 files changed

+132
-9
lines changed

3 files changed

+132
-9
lines changed

CHANGELOG.md

+12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
### 1.4.3
2+
3+
Disallow access to prototype chain (CVE-2024-54152) when using compile with locals (two arguments in the called function) :
4+
5+
```js
6+
compile("__proto__")({}, {});
7+
```
8+
9+
=> This now returns undefined, previously it would give you the `__proto__` instance which would allow Remote Code Execution.
10+
11+
Thanks to [@JorianWoltjer](https://github.com/JorianWoltjer) who found the vulnerability and reported it.
12+
113
### 1.4.2
214

315
Make `handleThis` the default if you use the `Lexer` and `Parser` directly, and you don't use `.compile`.

lib/parse.js

+51-7
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ if ("I".toLowerCase() !== "i") {
4242
lowercase = manualLowercase;
4343
}
4444

45+
// Run a function and disallow temporarly the use of the Function constructor
46+
// This makes arbitrary code generation attacks way more complicated.
47+
function runWithFunctionConstructorProtection(fn) {
48+
var originalFunctionConstructor = Function.prototype.constructor;
49+
delete Function.prototype.constructor;
50+
var result = fn();
51+
// eslint-disable-next-line no-extend-native
52+
Function.prototype.constructor = originalFunctionConstructor;
53+
return result;
54+
}
55+
4556
var jqLite, // delay binding since jQuery could be loaded after us.
4657
toString = Object.prototype.toString,
4758
getPrototypeOf = Object.getPrototypeOf,
@@ -1690,15 +1701,28 @@ ASTCompiler.prototype = {
16901701
extra +
16911702
this.watchFns() +
16921703
"return fn;";
1704+
16931705
// eslint-disable-next-line no-new-func
1694-
var fn = new Function(
1706+
var wrappedFn = new Function(
16951707
"$filter",
16961708
"getStringValue",
16971709
"ifDefined",
16981710
"plus",
16991711
fnString
17001712
)(this.$filter, getStringValue, ifDefined, plusFn);
17011713

1714+
var fn = function (s, l, a, i) {
1715+
return runWithFunctionConstructorProtection(function () {
1716+
return wrappedFn(s, l, a, i);
1717+
});
1718+
};
1719+
fn.assign = function (s, v, l) {
1720+
return runWithFunctionConstructorProtection(function () {
1721+
return wrappedFn.assign(s, v, l);
1722+
});
1723+
};
1724+
fn.inputs = wrappedFn.inputs;
1725+
17021726
this.state = this.stage = undefined;
17031727
fn.ast = ast;
17041728
fn.literal = isLiteral(ast);
@@ -1892,7 +1916,12 @@ ASTCompiler.prototype = {
18921916
);
18931917
},
18941918
intoId &&
1895-
self.lazyAssign(intoId, self.nonComputedMember("l", ast.name))
1919+
function () {
1920+
self.if_(
1921+
self.hasOwnProperty_("l", ast.name),
1922+
self.lazyAssign(intoId, self.nonComputedMember("l", ast.name))
1923+
);
1924+
}
18961925
);
18971926
recursionFn(intoId);
18981927
break;
@@ -2166,7 +2195,7 @@ ASTCompiler.prototype = {
21662195
},
21672196

21682197
filter: function (filterName) {
2169-
if (!this.state.filters.hasOwnProperty(filterName)) {
2198+
if (!hasOwnProperty.call(this.state.filters, filterName)) {
21702199
this.state.filters[filterName] = this.nextId(true);
21712200
}
21722201
return this.state.filters[filterName];
@@ -2258,7 +2287,7 @@ ASTCompiler.prototype = {
22582287
left +
22592288
"[" +
22602289
right +
2261-
"] : null)"
2290+
"] : undefined)"
22622291
);
22632292
},
22642293

@@ -2375,7 +2404,7 @@ ASTInterpreter.prototype = {
23752404
forEach(ast.body, function (expression) {
23762405
expressions.push(self.recurse(expression.expression));
23772406
});
2378-
var fn =
2407+
var wrappedFn =
23792408
ast.body.length === 0
23802409
? noop
23812410
: ast.body.length === 1
@@ -2389,10 +2418,22 @@ ASTInterpreter.prototype = {
23892418
};
23902419

23912420
if (assign) {
2392-
fn.assign = function (scope, value, locals) {
2421+
wrappedFn.assign = function (scope, value, locals) {
23932422
return assign(scope, locals, value);
23942423
};
23952424
}
2425+
2426+
var fn = function (scope, locals) {
2427+
return runWithFunctionConstructorProtection(function () {
2428+
return wrappedFn(scope, locals);
2429+
});
2430+
};
2431+
fn.assign = function (scope, value, locals) {
2432+
return runWithFunctionConstructorProtection(function () {
2433+
return wrappedFn.assign(scope, value, locals);
2434+
});
2435+
};
2436+
23962437
if (inputs) {
23972438
fn.inputs = inputs;
23982439
}
@@ -2720,7 +2761,10 @@ ASTInterpreter.prototype = {
27202761
if (create && create !== 1 && base && base[name] == null) {
27212762
base[name] = {};
27222763
}
2723-
var value = base ? base[name] : undefined;
2764+
var value;
2765+
if (base && hasOwnProperty.call(base, name)) {
2766+
value = base ? base[name] : undefined;
2767+
}
27242768
if (context) {
27252769
return { context: base, name: name, value: value };
27262770
}

test/main.test.js

+69-2
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,40 @@ describe("expressions", function () {
226226
expect(result).to.equal(undefined);
227227
});
228228

229+
it("should not leak indirectly with string concatenation with locals", function () {
230+
evaluate = compile(
231+
"a = null; a = ''['c'+'onstructor']['c'+'onstructor']; a = a('return process;'); a();",
232+
{ csp: true }
233+
);
234+
const result = evaluate({}, {});
235+
expect(result).to.equal(undefined);
236+
});
237+
229238
it("should not leak indirectly with literal string", function () {
230239
evaluate = compile(
231240
"a = null; a = ''['constructor']['constructor']; a = a('return process;'); a();"
232241
);
233242
const result = evaluate({});
234243
expect(result).to.equal(undefined);
235244
});
245+
246+
it("should not be able to rewrite hasOwnProperty", function () {
247+
const scope = {
248+
// Pre-condition: any function in scope that returns a truthy value
249+
func: function () {
250+
return "anything truthy";
251+
},
252+
};
253+
const options = {
254+
// Force to use ASTInterpreter
255+
csp: true,
256+
};
257+
const result = expressions.compile(
258+
"hasOwnProperty = func; constructor.getPrototypeOf(toString).constructor('return process')()",
259+
options
260+
)(scope, scope);
261+
expect(result).to.equal(undefined);
262+
});
236263
});
237264

238265
describe("when evaluating dot-notated assignments", function () {
@@ -676,6 +703,13 @@ describe("expressions", function () {
676703
});
677704
expect(evaluate(scope)).to.equal("myval");
678705
});
706+
707+
it("should be possible to calc this+this+this", function () {
708+
const evaluate = compile("this+this+this", {
709+
csp: false,
710+
});
711+
expect(evaluate(1)).to.equal(3);
712+
});
679713
});
680714

681715
describe("Equality", function () {
@@ -756,7 +790,7 @@ describe("expressions", function () {
756790

757791
it("should not leak with computed prop", function () {
758792
evaluate = compile("a['split']");
759-
expect(evaluate({ a: "" })).to.eql(null);
793+
expect(evaluate({ a: "" })).to.eql(undefined);
760794
});
761795

762796
it("should allow to read string length", function () {
@@ -782,11 +816,44 @@ describe("expressions", function () {
782816
);
783817
});
784818

785-
it("should work with __proto__", function () {
819+
it("should not show value of __proto__", function () {
786820
evaluate = compile("__proto__");
787821
expect(evaluate({})).to.eql(undefined);
788822
});
789823

824+
it("should not show value of __proto__ if passing context (second argument) with csp = false", function () {
825+
evaluate = compile("__proto__");
826+
expect(evaluate({}, {})).to.eql(undefined);
827+
});
828+
829+
it("should not show value of __proto__ if passing context (second argument) with csp = true", function () {
830+
evaluate = compile("__proto__", {
831+
csp: true,
832+
});
833+
expect(evaluate({}, {})).to.eql(undefined);
834+
});
835+
836+
it("should not show value of constructor if passing context (second argument) with csp = true", function () {
837+
evaluate = compile("constructor", {
838+
csp: true,
839+
});
840+
expect(evaluate({}, {})).to.eql(undefined);
841+
});
842+
843+
it("should not show value of this['__proto__'] if passing context (second argument) with csp = true", function () {
844+
evaluate = compile("this['__proto' + '__']", {
845+
csp: true,
846+
});
847+
expect(evaluate({}, {})).to.eql(undefined);
848+
});
849+
850+
it("should not show value of this['__proto__'] if passing context (second argument) with csp = false", function () {
851+
evaluate = compile("this['__proto' + '__']", {
852+
csp: false,
853+
});
854+
expect(evaluate({}, {})).to.eql(undefined);
855+
});
856+
790857
it("should work with toString", function () {
791858
evaluate = compile("toString");
792859
expect(evaluate({ toString: 10 })).to.eql(10);

0 commit comments

Comments
 (0)