豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 513ab2a

Browse files
committed
fix: improve the internal setSafeProperty to not allow setting properties other than numeric indices or length on arrays
1 parent 24d5ee7 commit 513ab2a

File tree

4 files changed

+195
-89
lines changed

4 files changed

+195
-89
lines changed

src/utils/customs.js

Lines changed: 61 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,83 @@
11
import { hasOwnProperty } from './object.js'
22

33
/**
4-
* Get a property of a plain object
4+
* Get a property of a plain object or array
55
* Throws an error in case the object is not a plain object or the
66
* property is not defined on the object itself
77
* @param {Object} object
88
* @param {string} prop
99
* @return {*} Returns the property value when safe
1010
*/
11-
function getSafeProperty (object, prop) {
12-
// only allow getting safe properties of a plain object
13-
if (isSafeProperty(object, prop)) {
11+
export function getSafeProperty (object, prop) {
12+
if (isSafeObjectProperty(object, prop) || isSafeArrayProperty(object, prop)) {
1413
return object[prop]
1514
}
1615

17-
if (typeof object[prop] === 'function' && isSafeMethod(object, prop)) {
18-
throw new Error('Cannot access method "' + prop + '" as a property')
16+
if (isSafeMethod(object, prop)) {
17+
throw new Error(`Cannot access method "${prop}" as a property`)
18+
}
19+
20+
if (object === null || object === undefined) {
21+
throw new TypeError(`Cannot access property "${prop}": object is ${object}`)
1922
}
2023

2124
throw new Error('No access to property "' + prop + '"')
2225
}
2326

2427
/**
25-
* Set a property on a plain object.
28+
* Set a property on a plain object or array.
2629
* Throws an error in case the object is not a plain object or the
2730
* property would override an inherited property like .constructor or .toString
2831
* @param {Object} object
2932
* @param {string} prop
3033
* @param {*} value
3134
* @return {*} Returns the value
3235
*/
33-
// TODO: merge this function into access.js?
34-
function setSafeProperty (object, prop, value) {
35-
// only allow setting safe properties of a plain object
36-
if (isSafeProperty(object, prop)) {
36+
export function setSafeProperty (object, prop, value) {
37+
if (isSafeObjectProperty(object, prop) || isSafeArrayProperty(object, prop)) {
3738
object[prop] = value
3839
return value
3940
}
4041

41-
throw new Error('No access to property "' + prop + '"')
42+
throw new Error(`No access to property "${prop}"`)
4243
}
4344

4445
/**
45-
* Test whether a property is safe to use on an object or Array.
46-
* For example .toString and .constructor are not safe
47-
* @param {Object | Array} object
46+
* Test whether a property is safe for reading and writing on an object
47+
* For example .constructor and .__proto__ are not safe
48+
* @param {Object} object
4849
* @param {string} prop
4950
* @return {boolean} Returns true when safe
5051
*/
51-
function isSafeProperty (object, prop) {
52-
if (!isPlainObject(object) && !Array.isArray(object)) {
52+
export function isSafeObjectProperty (object, prop) {
53+
if (!isPlainObject(object)) {
5354
return false
5455
}
55-
// SAFE: whitelisted
56-
// e.g length
57-
if (hasOwnProperty(safeNativeProperties, prop)) {
58-
return true
59-
}
60-
// UNSAFE: inherited from Object prototype
61-
// e.g constructor
62-
if (prop in Object.prototype) {
63-
// 'in' is used instead of hasOwnProperty for nodejs v0.10
64-
// which is inconsistent on root prototypes. It is safe
65-
// here because Object.prototype is a root object
66-
return false
67-
}
68-
// UNSAFE: inherited from Function prototype
69-
// e.g call, apply
70-
if (prop in Function.prototype) {
71-
// 'in' is used instead of hasOwnProperty for nodejs v0.10
72-
// which is inconsistent on root prototypes. It is safe
73-
// here because Function.prototype is a root object
56+
57+
return !(prop in Object.prototype)
58+
}
59+
60+
/**
61+
* Test whether a property is safe for reading and writing on an Array
62+
* For example .__proto__ and .constructor are not safe
63+
* @param {unknown} array
64+
* @param {string | number} prop
65+
* @return {boolean} Returns true when safe
66+
*/
67+
export function isSafeArrayProperty (array, prop) {
68+
if (!Array.isArray(array)) {
7469
return false
7570
}
76-
return true
71+
72+
return (
73+
typeof prop === 'number' ||
74+
(typeof prop === 'string' && isInteger(prop)) ||
75+
prop === 'length'
76+
)
77+
}
78+
79+
function isInteger (prop) {
80+
return /^\d+$/.test(prop)
7781
}
7882

7983
/**
@@ -83,7 +87,7 @@ function isSafeProperty (object, prop) {
8387
* @param {string} method
8488
* @return {function} Returns the method when valid
8589
*/
86-
function getSafeMethod (object, method) {
90+
export function getSafeMethod (object, method) {
8791
if (!isSafeMethod(object, method)) {
8892
throw new Error('No access to method "' + method + '"')
8993
}
@@ -98,22 +102,32 @@ function getSafeMethod (object, method) {
98102
* @param {string} method
99103
* @return {boolean} Returns true when safe, false otherwise
100104
*/
101-
function isSafeMethod (object, method) {
102-
if (object === null || object === undefined || typeof object[method] !== 'function') {
105+
export function isSafeMethod (object, method) {
106+
if (
107+
object === null ||
108+
object === undefined ||
109+
typeof object[method] !== 'function'
110+
) {
103111
return false
104112
}
113+
105114
// UNSAFE: ghosted
106115
// e.g overridden toString
107116
// Note that IE10 doesn't support __proto__ and we can't do this check there.
108-
if (hasOwnProperty(object, method) &&
109-
(Object.getPrototypeOf && (method in Object.getPrototypeOf(object)))) {
117+
if (
118+
hasOwnProperty(object, method) &&
119+
Object.getPrototypeOf &&
120+
method in Object.getPrototypeOf(object)
121+
) {
110122
return false
111123
}
124+
112125
// SAFE: whitelisted
113126
// e.g toString
114-
if (hasOwnProperty(safeNativeMethods, method)) {
127+
if (safeNativeMethods.has(method)) {
115128
return true
116129
}
130+
117131
// UNSAFE: inherited from Object prototype
118132
// e.g constructor
119133
if (method in Object.prototype) {
@@ -122,6 +136,7 @@ function isSafeMethod (object, method) {
122136
// here because Object.prototype is a root object
123137
return false
124138
}
139+
125140
// UNSAFE: inherited from Function prototype
126141
// e.g call, apply
127142
if (method in Function.prototype) {
@@ -130,27 +145,12 @@ function isSafeMethod (object, method) {
130145
// here because Function.prototype is a root object
131146
return false
132147
}
148+
133149
return true
134150
}
135151

136-
function isPlainObject (object) {
152+
export function isPlainObject (object) {
137153
return typeof object === 'object' && object && object.constructor === Object
138154
}
139155

140-
const safeNativeProperties = {
141-
length: true,
142-
name: true
143-
}
144-
145-
const safeNativeMethods = {
146-
toString: true,
147-
valueOf: true,
148-
toLocaleString: true
149-
}
150-
151-
export { getSafeProperty }
152-
export { setSafeProperty }
153-
export { isSafeProperty }
154-
export { getSafeMethod }
155-
export { isSafeMethod }
156-
export { isPlainObject }
156+
const safeNativeMethods = new Set(['toString', 'valueOf', 'toLocaleString'])

src/utils/map.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { getSafeProperty, isSafeProperty, setSafeProperty } from './customs.js'
1+
import {
2+
getSafeProperty,
3+
isSafeObjectProperty,
4+
setSafeProperty
5+
} from './customs.js'
26
import { isMap, isObject } from './is.js'
37

48
/**
@@ -32,7 +36,7 @@ export class ObjectWrappingMap {
3236
}
3337

3438
has (key) {
35-
return isSafeProperty(this.wrappedObject, key) && key in this.wrappedObject
39+
return isSafeObjectProperty(this.wrappedObject, key) && key in this.wrappedObject
3640
}
3741

3842
entries () {
@@ -46,7 +50,7 @@ export class ObjectWrappingMap {
4650
}
4751

4852
delete (key) {
49-
if (isSafeProperty(this.wrappedObject, key)) {
53+
if (isSafeObjectProperty(this.wrappedObject, key)) {
5054
delete this.wrappedObject[key]
5155
}
5256
}

test/unit-tests/expression/security.test.js

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,55 @@ describe('security', function () {
7676
it('should not allow calling Function via Object.assign', function () {
7777
// TODO: simplify this test case, let it output console.log('hacked...')
7878
assert.throws(function () {
79-
math.evaluate('{}.constructor.assign(cos.constructor, {binding: cos.bind})\n' +
79+
math.evaluate(
80+
'{}.constructor.assign(cos.constructor, {binding: cos.bind})\n' +
8081
'{}.constructor.assign(cos.constructor, {bind: null})\n' +
8182
'f=cos.constructor.binding()("console.log(\'hacked...\')")\n' +
82-
'f()')
83-
}, /Error: No access to property "bind/)
83+
'f()'
84+
)
85+
}, /Error: No access to property "constructor/)
86+
})
87+
88+
it('should not allow misusing setSafeProperty to set properties on Arrays', function () {
89+
assert.throws(function () {
90+
const exploit = `
91+
constantNode = reviver('',{'mathjs':'ConstantNode','value':1});
92+
93+
# get a reference to a raw JavaScript array
94+
array = reviver('',{'mathjs':'ArrayNode'}).map().toJSON()['items'];
95+
array.push({'name':'a','type':{}});
96+
97+
# override params.map to hijack this.params and this.types
98+
array.map = f(callback)=callback({'name':{'name':'a','map':f2(callback2)=callback2({},'constructor')},'type':cos});
99+
100+
# trigger callback and get the pointer to Function.constructor
101+
functionAssignmentNode = reviver('',{'mathjs':'FunctionAssignmentNode','name':'a','params':array,'expr':constantNode});
102+
func = functionAssignmentNode.toJSON()['params']['type'];
103+
104+
getProcess = func('return process');
105+
getProcess()`
106+
107+
console.log(
108+
'Hacked! node.js version:',
109+
math.evaluate(exploit).entries[0].version
110+
)
111+
}, /Error: No access to property "map"/)
112+
})
113+
114+
it('should not allow getting access to constructor via DenseMatrix.get index argument', function () {
115+
assert.throws(function () {
116+
const exploit = `
117+
m = matrix();
118+
func = m.get({"length":1,"reduce":f(callback,a)=callback(cos,"constructor")});
119+
getProcess = func("return process");
120+
getProcess()
121+
`
122+
123+
console.log(
124+
'Hacked! node.js version:',
125+
math.evaluate(exploit).entries[0].version
126+
)
127+
}, /Error: Array expected for index/)
84128
})
85129

86130
it('should not allow disguising forbidden properties with unicode characters', function () {

0 commit comments

Comments
 (0)