Skip to content

Commit 08158db

Browse files
committed
🐛 fix getStaticValue security issue
1 parent 587cca2 commit 08158db

File tree

2 files changed

+131
-15
lines changed

2 files changed

+131
-15
lines changed

src/get-static-value.js

+102-13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* globals BigInt */
2+
13
import { findVariable } from "./find-variable"
24

35
const builtinNames = Object.freeze(
@@ -14,9 +16,7 @@ const builtinNames = Object.freeze(
1416
"decodeURIComponent",
1517
"encodeURI",
1618
"encodeURIComponent",
17-
"Error",
1819
"escape",
19-
"EvalError",
2020
"Float32Array",
2121
"Float64Array",
2222
"Function",
@@ -37,26 +37,97 @@ const builtinNames = Object.freeze(
3737
"parseInt",
3838
"Promise",
3939
"Proxy",
40-
"RangeError",
41-
"ReferenceError",
4240
"Reflect",
4341
"RegExp",
4442
"Set",
4543
"String",
4644
"Symbol",
47-
"SyntaxError",
48-
"TypeError",
4945
"Uint16Array",
5046
"Uint32Array",
5147
"Uint8Array",
5248
"Uint8ClampedArray",
5349
"undefined",
5450
"unescape",
55-
"URIError",
5651
"WeakMap",
5752
"WeakSet",
5853
])
5954
)
55+
const callAllowed = new Set(
56+
[
57+
Array.isArray,
58+
typeof BigInt === "function" ? BigInt : undefined,
59+
Boolean,
60+
Date,
61+
Date.parse,
62+
decodeURI,
63+
decodeURIComponent,
64+
encodeURI,
65+
encodeURIComponent,
66+
escape,
67+
isFinite,
68+
isNaN,
69+
isPrototypeOf,
70+
...Object.getOwnPropertyNames(Math)
71+
.map(k => Math[k])
72+
.filter(f => typeof f === "function"),
73+
Number,
74+
Number.isFinite,
75+
Number.isNaN,
76+
Number.parseFloat,
77+
Number.parseInt,
78+
Object,
79+
Object.entries, //eslint-disable-line @mysticatea/node/no-unsupported-features/es-builtins
80+
Object.is,
81+
Object.isExtensible,
82+
Object.isFrozen,
83+
Object.isSealed,
84+
Object.keys,
85+
Object.values, //eslint-disable-line @mysticatea/node/no-unsupported-features/es-builtins
86+
parseFloat,
87+
parseInt,
88+
RegExp,
89+
String,
90+
String.fromCharCode,
91+
String.fromCodePoint,
92+
String.raw,
93+
Symbol,
94+
Symbol.for,
95+
Symbol.keyFor,
96+
unescape,
97+
].filter(f => typeof f === "function")
98+
)
99+
const callPassThrough = new Set([
100+
Object.freeze,
101+
Object.preventExtensions,
102+
Object.seal,
103+
])
104+
105+
/**
106+
* Get the property descriptor.
107+
* @param {object} object The object to get.
108+
* @param {string|number|symbol} name The property name to get.
109+
*/
110+
function getPropertyDescriptor(object, name) {
111+
let x = object
112+
while ((typeof x === "object" || typeof x === "function") && x !== null) {
113+
const d = Object.getOwnPropertyDescriptor(x, name)
114+
if (d) {
115+
return d
116+
}
117+
x = Object.getPrototypeOf(x)
118+
}
119+
return null
120+
}
121+
122+
/**
123+
* Check if a property is getter or not.
124+
* @param {object} object The object to check.
125+
* @param {string|number|symbol} name The property name to check.
126+
*/
127+
function isGetter(object, name) {
128+
const d = getPropertyDescriptor(object, name)
129+
return d != null && d.get != null
130+
}
60131

61132
/**
62133
* Get the element values of a given node list.
@@ -176,13 +247,23 @@ const operations = Object.freeze({
176247
if (object != null && property != null) {
177248
const receiver = object.value
178249
const methodName = property.value
179-
return { value: receiver[methodName](...args) }
250+
if (callAllowed.has(receiver[methodName])) {
251+
return { value: receiver[methodName](...args) }
252+
}
253+
if (callPassThrough.has(receiver[methodName])) {
254+
return { value: args[0] }
255+
}
180256
}
181257
} else {
182258
const callee = getStaticValueR(calleeNode, initialScope)
183259
if (callee != null) {
184260
const func = callee.value
185-
return { value: func(...args) }
261+
if (callAllowed.has(func)) {
262+
return { value: func(...args) }
263+
}
264+
if (callPassThrough.has(func)) {
265+
return { value: args[0] }
266+
}
186267
}
187268
}
188269
}
@@ -240,7 +321,7 @@ const operations = Object.freeze({
240321
// It was a RegExp/BigInt literal, but Node.js didn't support it.
241322
return null
242323
}
243-
return node
324+
return { value: node.value }
244325
},
245326

246327
LogicalExpression(node, initialScope) {
@@ -268,7 +349,11 @@ const operations = Object.freeze({
268349
? getStaticValueR(node.property, initialScope)
269350
: { value: node.property.name }
270351

271-
if (object != null && property != null) {
352+
if (
353+
object != null &&
354+
property != null &&
355+
!isGetter(object.value, property.value)
356+
) {
272357
return { value: object.value[property.value] }
273358
}
274359
return null
@@ -280,7 +365,9 @@ const operations = Object.freeze({
280365

281366
if (callee != null && args != null) {
282367
const Func = callee.value
283-
return { value: new Func(...args) }
368+
if (callAllowed.has(Func)) {
369+
return { value: new Func(...args) }
370+
}
284371
}
285372

286373
return null
@@ -339,7 +426,9 @@ const operations = Object.freeze({
339426
const strings = node.quasi.quasis.map(q => q.value.cooked)
340427
strings.raw = node.quasi.quasis.map(q => q.value.raw)
341428

342-
return { value: func(strings, ...expressions) }
429+
if (func === String.raw) {
430+
return { value: func(strings, ...expressions) }
431+
}
343432
}
344433

345434
return null

test/get-static-value.js

+29-2
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe("The 'getStaticValue' function", () => {
7575
{ code: "Symbol[iterator]", expected: null },
7676
{ code: "Object.freeze", expected: { value: Object.freeze } },
7777
{ code: "Object.xxx", expected: { value: undefined } },
78-
{ code: "new Array(2)", expected: { value: new Array(2) } },
78+
{ code: "new Array(2)", expected: null },
7979
{ code: "new Array(len)", expected: null },
8080
{ code: "({})", expected: { value: {} } },
8181
{
@@ -116,6 +116,33 @@ const aMap = Object.freeze({
116116
;\`on\${eventName} : \${aMap[eventName]}\``,
117117
expected: { value: "onclick : 777" },
118118
},
119+
{
120+
code: 'Function("return process.env.npm_name")()',
121+
expected: null,
122+
},
123+
{
124+
code: 'new Function("return process.env.npm_name")()',
125+
expected: null,
126+
},
127+
{
128+
code:
129+
'({}.constructor.constructor("return process.env.npm_name")())',
130+
expected: null,
131+
},
132+
{
133+
code:
134+
'JSON.stringify({a:1}, new {}.constructor.constructor("console.log(\\"code injected\\"); process.exit(1)"), 2)',
135+
expected: null,
136+
},
137+
{
138+
code:
139+
'Object.create(null, {a:{get:new {}.constructor.constructor("console.log(\\"code injected\\"); process.exit(1)")}}).a',
140+
expected: null,
141+
},
142+
{
143+
code: "RegExp.$1",
144+
expected: null,
145+
},
119146
]) {
120147
it(`should return ${JSON.stringify(expected)} from ${code}`, () => {
121148
const linter = new eslint.Linter()
@@ -138,7 +165,7 @@ const aMap = Object.freeze({
138165
if (actual == null) {
139166
assert.strictEqual(actual, expected)
140167
} else {
141-
assert.deepStrictEqual(actual.value, expected.value)
168+
assert.deepStrictEqual(actual, expected)
142169
}
143170
})
144171
}

0 commit comments

Comments
 (0)