Skip to content

Commit 2f45600

Browse files
committed
Fixed a security vulnerability in the expression parser allowing execution of arbitrary JavaScript
1 parent 691d555 commit 2f45600

8 files changed

Lines changed: 111 additions & 4 deletions

File tree

HISTORY.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# History
22

33

4+
## 2017-03-31, version 3.10.2
5+
6+
- Fixed a security vulnerability in the expression parser allowing
7+
execution of arbitrary JavaScript. Thanks @CapacitorSet and @denvit.
8+
9+
410
## 2017-03-26, version 3.10.1
511

612
- Fixed `xgcd` for negative values. Thanks @litmit.

docs/expressions/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ This section is divided in the following pages:
1818
- [Algebra](algebra.md) describing symbolic computation in math.js.
1919
- [Customization](customization.md) describes how to customize processing and
2020
evaluation of expressions.
21+
- [Security](security.md) about security risks of executing arbitrary
22+
expressions.

docs/expressions/security.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Security
2+
3+
Executing arbitrary expressions like enabled by the expression parser of
4+
mathjs involves a risk in general. When you're using mathjs to let users
5+
execute arbitrary expressions, it's good to take a moment to think about
6+
possible security and stability implications.
7+
8+
## Security risks
9+
10+
A user could try to inject malicious JavaScript code via the expression
11+
parser. The expression parser of mathjs offers a sandboxed environment
12+
to execute expressions which should make this impossible. It's possible
13+
though that there is an unknown security hole, so it's important to be
14+
careful, especially when allowing server side execution of arbitrary
15+
expressions.
16+
17+
The expression parser of mathjs parses the input in a controlled
18+
way into an expression tree, then compiles it into fast performing
19+
JavaScript using JavaScript's `eval` before actually evaluating the
20+
expression. The parser actively prevents access to JavaScripts internal
21+
`eval` and `new Function` which are the main cause of security attacks.
22+
23+
When running a node.js server, it's good to be aware of the different
24+
types of security risks. The risk whe running inside a browser may be
25+
limited though it's good to be aware of [Cross side scripting (XSS)](https://www.wikiwand.com/en/Cross-site_scripting) vulnerabilities. A nice overview of
26+
security risks of a node.js servers is listed in an article [Node.js security checklist](https://blog.risingstack.com/node-js-security-checklist/) by Gergely Nemeth.
27+
Lastly, one could look into running server side code in a sandboxed
28+
[node.js VM](https://nodejs.org/api/vm.html).
29+
30+
## Stability risks
31+
32+
A user could accidentally or on purpose execute a
33+
heavy expression like creating a huge matrix. That can let the
34+
JavaScript engine run out of memory or freeze it when the CPU goes
35+
to 100% for a long time.
36+
37+
To protect against this sort of issue, one can run the expression parser
38+
in a separate Web Worker or child_process, so it can't affect the
39+
main process. The workers can be killed when it runs for too
40+
long or consumes too much memory. A useful library in this regard
41+
is [workerpool](https://github.com/josdejong/workerpool), which makes
42+
it easy to manage a pool of workers in both browser and node.js.

lib/expression/function/eval.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ function factory (type, config, load, typed) {
88
/**
99
* Evaluate an expression.
1010
*
11+
* Note the evaluating arbitrary expressions may involve security risks,
12+
* see [http://mathjs.org/docs/expressions/security.html](http://mathjs.org/docs/expressions/security.html) for more information.
13+
*
1114
* Syntax:
1215
*
1316
* math.eval(expr)

lib/expression/function/parse.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ function factory (type, config, load, typed) {
77
* Parse an expression. Returns a node tree, which can be evaluated by
88
* invoking node.eval();
99
*
10+
* Note the evaluating arbitrary expressions may involve security risks,
11+
* see [http://mathjs.org/docs/expressions/security.html](http://mathjs.org/docs/expressions/security.html) for more information.
12+
*
1013
* Syntax:
1114
*
1215
* math.parse(expr)

lib/expression/node/FunctionNode.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ function factory (type, config, load, typed, math) {
7070
* @private
7171
*/
7272
FunctionNode.prototype._compile = function (defs, args) {
73+
defs.customs = customs;
74+
7375
// compile fn and arguments
7476
var jsFn = this.fn._compile(defs, args);
7577
var jsArgs = this.args.map(function (arg) {
@@ -89,11 +91,11 @@ function factory (type, config, load, typed, math) {
8991
argsName = this._getUniqueArgumentsName(defs);
9092
defs[argsName] = this.args;
9193

92-
return jsFn + '(' + argsName + ', math, ' + jsScope + ')';
94+
return 'customs(' + jsFn + ')(' + argsName + ', math, ' + jsScope + ')';
9395
}
9496
else {
9597
// "regular" evaluation
96-
return jsFn + '(' + jsArgs.join(', ') + ')';
98+
return 'customs(' + jsFn + ')(' + jsArgs.join(', ') + ')';
9799
}
98100
}
99101
else if (this.fn.isAccessorNode && this.fn.index.isObjectProperty()) {
@@ -106,6 +108,7 @@ function factory (type, config, load, typed, math) {
106108

107109
return '(function () {' +
108110
'var object = ' + jsObject + ';' +
111+
'customs(object["' + prop + '"]);' +
109112
'return (object["' + prop + '"] && object["' + prop + '"].rawArgs) ' +
110113
' ? object["' + prop + '"](' + argsName + ', math, ' + jsScope + ')' +
111114
' : object["' + prop + '"](' + jsArgs.join(', ') + ')' +
@@ -117,7 +120,7 @@ function factory (type, config, load, typed, math) {
117120
defs[argsName] = this.args;
118121

119122
return '(function () {' +
120-
'var fn = ' + jsFn + ';' +
123+
'var fn = customs(' + jsFn + ');' +
121124
'return (fn && fn.rawArgs) ' +
122125
' ? fn(' + argsName + ', math, ' + jsScope + ')' +
123126
' : fn(' + jsArgs.join(', ') + ')' +
@@ -414,6 +417,31 @@ function factory (type, config, load, typed, math) {
414417
return FunctionNode;
415418
}
416419

420+
/**
421+
* Don't allow access to Function and eval, which can be used
422+
* to execute arbitrary JavaScript code. Example of this vulnerability:
423+
*
424+
* math.eval('[].map.constructor("console.log(\\"hacked...\\")")()')
425+
*
426+
* @param {function} fn
427+
* @return {function} Returns the input function when ok,
428+
* else throws an error
429+
*/
430+
function customs (fn) {
431+
if (fn === Function) {
432+
throw new Error('Calling "Function" is not allowed');
433+
}
434+
435+
if (fn === jsEval) {
436+
throw new Error('Calling "eval" is not allowed');
437+
}
438+
439+
// this function is ok
440+
return fn;
441+
}
442+
443+
var jsEval = eval
444+
417445
exports.name = 'FunctionNode';
418446
exports.path = 'expression.node';
419447
exports.math = true; // request access to the math namespace as 5th argument of the factory function

lib/expression/node/utils/access.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function factory (type, config, load, typed) {
3131
}
3232
else if (typeof object === 'object') {
3333
if (!index.isObjectProperty()) {
34-
throw TypeError('Cannot apply a numeric index as object property');
34+
throw new TypeError('Cannot apply a numeric index as object property');
3535
}
3636
return object[index.getObjectProperty()];
3737
}

test/expression/parse.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2041,4 +2041,27 @@ describe('parse', function() {
20412041

20422042
});
20432043

2044+
describe('security', function () {
2045+
2046+
it ('should not allow calling Function from an object property', function () {
2047+
assert.throws(function () {
2048+
console.log(math.eval('[].map.constructor("console.log(\\"hacked...\\")")()'))
2049+
math.eval('[].map.constructor("console.log(\\"hacked...\\")")()')
2050+
}, /Error: Calling "Function" is not allowed/)
2051+
})
2052+
2053+
it ('should not allow calling Function', function () {
2054+
assert.throws(function () {
2055+
math.eval('disguised("console.log(\\"hacked...\\")")()', {disguised: Function})
2056+
}, /Error: Calling "Function" is not allowed/)
2057+
})
2058+
2059+
it ('should not allow calling eval', function () {
2060+
assert.throws(function () {
2061+
math.eval('disguised("console.log(\\"hacked...\\")")()', {disguised: eval})
2062+
}, /Error: Calling "eval" is not allowed/)
2063+
})
2064+
2065+
});
2066+
20442067
});

0 commit comments

Comments
 (0)