Skip to content

Commit

Permalink
fix: security vulnerability in FunctionNode and SymbolNode allowi…
Browse files Browse the repository at this point in the history
…ng arbitrary code execution via `math.evaluate`
  • Loading branch information
josdejong committed Jul 24, 2023
1 parent d0d11a2 commit 6dcbc6b
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 22 deletions.
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

# unpublished changes since 11.9.0

- Fix a security vulnerability in `FunctionNode` and `SymbolNode` allowing
arbitrary code execution via `math.evaluate`. Thanks Harry Chen.
- Fix #3001: mathjs bundle containing `new Function(...)` (CSP issue).


Expand Down
17 changes: 7 additions & 10 deletions src/expression/node/FunctionNode.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isAccessorNode, isFunctionAssignmentNode, isIndexNode, isNode, isSymbolNode } from '../../utils/is.js'
import { escape, format } from '../../utils/string.js'
import { hasOwnProperty } from '../../utils/object.js'
import { getSafeProperty, validateSafeMethod } from '../../utils/customs.js'
import { getSafeProperty, getSafeMethod } from '../../utils/customs.js'
import { createSubScope } from '../../utils/scope.js'
import { factory } from '../../utils/factory.js'
import { defaultTemplate, latexFunctions } from '../../utils/latex.js'
Expand Down Expand Up @@ -205,7 +205,7 @@ export const createFunctionNode = /* #__PURE__ */ factory(name, dependencies, ({
} else { // the function symbol is an argName
const rawArgs = this.args
return function evalFunctionNode (scope, args, context) {
const fn = args[name]
const fn = getSafeProperty(args, name)
if (typeof fn !== 'function') {
throw new TypeError(
`Argument '${name}' was not a function; received: ${strin(fn)}`
Expand Down Expand Up @@ -235,18 +235,15 @@ export const createFunctionNode = /* #__PURE__ */ factory(name, dependencies, ({

return function evalFunctionNode (scope, args, context) {
const object = evalObject(scope, args, context)
validateSafeMethod(object, prop)
const isRaw = object[prop] && object[prop].rawArgs
const fn = getSafeMethod(object, prop)

if (isRaw) {
if (fn?.rawArgs) {
// "Raw" evaluation
return object[prop](
rawArgs, math, createSubScope(scope, args), scope)
return fn(rawArgs, math, createSubScope(scope, args), scope)
} else {
// "regular" evaluation
const values = evalArgs.map(
(evalArg) => evalArg(scope, args, context))
return object[prop].apply(object, values)
const values = evalArgs.map((evalArg) => evalArg(scope, args, context))
return fn.apply(object, values)
}
}
} else {
Expand Down
12 changes: 5 additions & 7 deletions src/expression/node/ObjectNode.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { getSafeProperty } from '../../utils/customs.js'
import { factory } from '../../utils/factory.js'
import { isNode } from '../../utils/is.js'
import { escape, stringify } from '../../utils/string.js'
import { isSafeProperty } from '../../utils/customs.js'
import { hasOwnProperty } from '../../utils/object.js'
import { factory } from '../../utils/factory.js'
import { escape, stringify } from '../../utils/string.js'

const name = 'ObjectNode'
const dependencies = [
Expand Down Expand Up @@ -58,11 +58,9 @@ export const createObjectNode = /* #__PURE__ */ factory(name, dependencies, ({ N
// so you cannot create a key like {"co\\u006Estructor": null}
const stringifiedKey = stringify(key)
const parsedKey = JSON.parse(stringifiedKey)
if (!isSafeProperty(this.properties, parsedKey)) {
throw new Error('No access to property "' + parsedKey + '"')
}
const prop = getSafeProperty(this.properties, key)

evalEntries[parsedKey] = this.properties[key]._compile(math, argNames)
evalEntries[parsedKey] = prop._compile(math, argNames)
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/expression/node/SymbolNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ m
// (like an x when inside the expression of a function
// assignment `f(x) = ...`)
return function (scope, args, context) {
return args[name]
return getSafeProperty(args, name)
}
} else if (name in math) {
return function (scope, args, context) {
Expand Down
8 changes: 5 additions & 3 deletions src/utils/customs.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,14 @@ function isSafeProperty (object, prop) {
* Throws an error when that's not the case.
* @param {Object} object
* @param {string} method
* @return {function} Returns the method when valid
*/
// TODO: merge this function into assign.js?
function validateSafeMethod (object, method) {
function getSafeMethod (object, method) {
if (!isSafeMethod(object, method)) {
throw new Error('No access to method "' + method + '"')
}

return object[method]
}

/**
Expand Down Expand Up @@ -158,6 +160,6 @@ export { setSafeProperty }
export { isSafeProperty }
export { hasSafeProperty }
export { getSafeProperties }
export { validateSafeMethod }
export { getSafeMethod }
export { isSafeMethod }
export { isPlainObject }
10 changes: 10 additions & 0 deletions test/unit-tests/expression/security.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,16 @@ describe('security', function () {
assert.throws(function () { math.evaluate('unit("5cm").valueOf') }, /Cannot access method "valueOf" as a property/)
})

it('should not allow accessing constructor via FunctionNode.name', function () {
assert.throws(function () {
// could execute a nodejs script like "return process.mainModule.require(child_process).execSync(whoami)"
const result = math.evaluate('evalFunctionNode=parse("constructor(\'return process.version\')")._compile({},{});' +
'f=evalFunctionNode(null,cos);' +
'f()')
console.warn('Hacked! node.js version:', result.entries[0])
}, /No access to property "constructor"/)
})

it('should not have access to specific namespaces', function () {
Object.keys(math.expression.mathWithTransform).forEach(function (name) {
const value = math.expression.mathWithTransform[name]
Expand Down
48 changes: 47 additions & 1 deletion test/unit-tests/utils/customs.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// test boolean utils
import assert from 'assert'

import { isPlainObject, isSafeMethod, isSafeProperty } from '../../../src/utils/customs.js'
import {
getSafeMethod,
getSafeProperty,
isPlainObject,
isSafeMethod,
isSafeProperty
} from '../../../src/utils/customs.js'
import math from '../../../src/defaultInstance.js'

describe('customs', function () {
Expand Down Expand Up @@ -110,6 +116,20 @@ describe('customs', function () {
})
})

describe('getSafeMethod', function () {
it('should return a method when safe', function () {
const obj = { getName: () => 'Joe' }

assert.strictEqual(getSafeMethod(obj, 'getName'), obj.getName)
})

it('should throw an exception when a method is unsafe', function () {
assert.throws(() => {
getSafeMethod(Function, 'constructor')
}, /Error: No access to method "constructor"/)
})
})

describe('isSafeProperty', function () {
it('should test properties on plain objects', function () {
const object = {}
Expand Down Expand Up @@ -160,6 +180,32 @@ describe('customs', function () {
})
})

describe('getSafeProperty', function () {
it('should return a method when safe', function () {
const obj = { getName: () => 'Joe' }

assert.strictEqual(getSafeProperty(obj, 'getName'), obj.getName)
})

it('should return a property when safe', function () {
const obj = { username: 'Joe' }

assert.strictEqual(getSafeProperty(obj, 'username'), 'Joe')
})

it('should throw an exception when a method is unsafe', function () {
assert.throws(() => {
getSafeProperty(Function, 'constructor')
}, /Error: No access to property "constructor"/)
})

it('should throw an exception when a property is unsafe', function () {
assert.throws(() => {
getSafeProperty({ constructor: 'test' }, 'constructor')
}, /Error: No access to property "constructor"/)
})
})

it('should distinguish plain objects', function () {
const a = {}
const b = Object.create(a)
Expand Down

0 comments on commit 6dcbc6b

Please sign in to comment.