Skip to content

Commit 4a15bd0

Browse files
committed
refactor: consider Function("return this") as safe
1 parent d26eafc commit 4a15bd0

File tree

10 files changed

+213
-20
lines changed

10 files changed

+213
-20
lines changed

src/probes/isUnsafeCallee.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ function validateNode(node) {
1515
function main(node, options) {
1616
const { analysis, data: calleeName } = options;
1717

18+
if (
19+
calleeName === "Function" &&
20+
node.callee.arguments.length > 0 &&
21+
node.callee.arguments[0].value === "return this"
22+
) {
23+
return ProbeSignals.Skip;
24+
}
1825
analysis.addWarning("unsafe-stmt", calleeName, node.loc);
1926

2027
return ProbeSignals.Skip;

test/probes/isUnsafeCallee.spec.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,19 @@ test("should detect eval", () => {
2121
assert.equal(result.value, "eval");
2222
});
2323

24-
test("should detect Function", () => {
24+
test("should not detect warnings for Function with return this", () => {
2525
const str = "Function(\"return this\")()";
2626

27+
const ast = parseScript(str);
28+
const sastAnalysis = getSastAnalysis(str, isUnsafeCallee)
29+
.execute(ast.body);
30+
31+
assert.strictEqual(sastAnalysis.warnings.length, 0);
32+
});
33+
34+
test("should detect for unsafe Function statement", () => {
35+
const str = "Function(\"anything in here\")()";
36+
2737
const ast = parseScript(str);
2838
const sastAnalysis = getSastAnalysis(str, isUnsafeCallee)
2939
.execute(ast.body);

test/runASTAnalysis.spec.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,27 @@ test("it should be capable to follow a malicious code with hexa computation and
9595
assert.deepEqual([...dependencies.keys()], ["./test/data"]);
9696
});
9797

98+
test.skip("it should be capable to follow a malicious code with return Function this", () => {
99+
const { warnings, dependencies } = runASTAnalysis(`
100+
function unhex(r) {
101+
return Buffer.from(r, "hex").toString();
102+
}
103+
104+
const g = freeGlobal || freeSelf || Function('return this')();
105+
const p = g["pro" + "cess"];
106+
107+
const evil = p["mainMod" + "ule"][unhex("72657175697265")];
108+
const work = evil(unhex("2e2f746573742f64617461"));
109+
`);
110+
111+
assert.deepEqual(getWarningKind(warnings), [
112+
"encoded-literal",
113+
"unsafe-import",
114+
"unsafe-stmt"
115+
].sort());
116+
assert.deepEqual([...dependencies.keys()], ["./test/data"]);
117+
});
118+
98119
test("it should throw a 'short-identifiers' warning for a code with only one-character identifiers", () => {
99120
const { warnings } = runASTAnalysis(`
100121
var a = 0, b, c, d;

workspaces/estree-ast-utils/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,27 @@ Return `true` if the given Node is a Literal Regex Node.
115115

116116
</details>
117117

118+
<details><summary>extractLogicalExpression(node)</summary>
119+
120+
Extract all LogicalExpression recursively and return an IterableIterator of
121+
122+
```ts
123+
{ operator: "||" | "&&" | "??", node: any }
124+
```
125+
126+
For the following code example
127+
128+
```js
129+
freeGlobal || freeSelf || Function('return this')();
130+
```
131+
132+
The extract will return three parts
133+
- freeGlobal
134+
- freeSelf
135+
- and finally `Function('return this')();`
136+
137+
</details>
138+
118139
## License
119140

120141
MIT
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
export function* extractLogicalExpression(
3+
node
4+
) {
5+
if (node.type !== "LogicalExpression") {
6+
return;
7+
}
8+
9+
if (node.left.type === "LogicalExpression") {
10+
yield* extractLogicalExpression(node.left);
11+
}
12+
else {
13+
yield { operator: node.operator, node: node.left };
14+
}
15+
16+
if (node.right.type === "LogicalExpression") {
17+
yield* extractLogicalExpression(node.right);
18+
}
19+
else {
20+
yield { operator: node.operator, node: node.right };
21+
}
22+
}

workspaces/estree-ast-utils/src/index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { VariableTracer } from "./utils/VariableTracer";
33

44
export { VariableTracer };
55

6+
export function extractLogicalExpression(
7+
node: any
8+
): IterableIterator<{ operator: "||" | "&&" | "??", node: any }>;
9+
610
export function arrayExpressionToString(
711
node: any, options?: { tracer?: VariableTracer }
812
): IterableIterator<string>;

workspaces/estree-ast-utils/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ export * from "./getCallExpressionArguments.js";
55
export * from "./concatBinaryExpression.js";
66
export * from "./arrayExpressionToString.js";
77
export * from "./isLiteralRegex.js";
8+
export * from "./extractLogicalExpression.js";
89

910
export * from "./utils/VariableTracer.js";

workspaces/estree-ast-utils/src/utils/VariableTracer.js

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getMemberExpressionIdentifier } from "../getMemberExpressionIdentifier.
99
import { getCallExpressionIdentifier } from "../getCallExpressionIdentifier.js";
1010
import { getVariableDeclarationIdentifiers } from "../getVariableDeclarationIdentifiers.js";
1111
import { getCallExpressionArguments } from "../getCallExpressionArguments.js";
12+
import { extractLogicalExpression } from "../extractLogicalExpression.js";
1213

1314
// CONSTANTS
1415
const kGlobalIdentifiersToTrace = new Set([
@@ -233,10 +234,8 @@ export class VariableTracer extends EventEmitter {
233234
}
234235
}
235236

236-
#walkRequireCallExpression(variableDeclaratorNode) {
237-
const { init, id } = variableDeclaratorNode;
238-
239-
const moduleNameLiteral = init.arguments
237+
#walkRequireCallExpression(node, id) {
238+
const moduleNameLiteral = node.arguments
240239
.find((argumentNode) => argumentNode.type === "Literal" && this.#traced.has(argumentNode.value));
241240
if (!moduleNameLiteral) {
242241
return;
@@ -256,17 +255,48 @@ export class VariableTracer extends EventEmitter {
256255
}
257256

258257
#walkVariableDeclarationWithIdentifier(variableDeclaratorNode) {
259-
const { init, id } = variableDeclaratorNode;
258+
const { init } = variableDeclaratorNode;
260259

261260
switch (init.type) {
261+
/**
262+
* var root = freeGlobal || freeSelf || Function('return this')();
263+
*/
264+
case "LogicalExpression": {
265+
for (const { node } of extractLogicalExpression(init)) {
266+
this.#walkVariableDeclarationInitialization(
267+
variableDeclaratorNode,
268+
node
269+
);
270+
}
271+
272+
return void 0;
273+
}
274+
275+
default:
276+
return this.#walkVariableDeclarationInitialization(
277+
variableDeclaratorNode
278+
);
279+
}
280+
}
281+
282+
#walkVariableDeclarationInitialization(
283+
variableDeclaratorNode,
284+
childNode = variableDeclaratorNode.init
285+
) {
286+
const { id } = variableDeclaratorNode;
287+
288+
switch (childNode.type) {
262289
// let foo = "10"; <-- "foo" is the key and "10" the value
263290
case "Literal":
264-
this.literalIdentifiers.set(id.name, init.value);
291+
this.literalIdentifiers.set(id.name, childNode.value);
265292
break;
266293

267-
// const g = eval("this");
294+
/**
295+
* const g = eval("this");
296+
* const g = Function("return this")();
297+
*/
268298
case "CallExpression": {
269-
const fullIdentifierPath = getCallExpressionIdentifier(init);
299+
const fullIdentifierPath = getCallExpressionIdentifier(childNode);
270300
if (fullIdentifierPath === null) {
271301
break;
272302
}
@@ -277,18 +307,18 @@ export class VariableTracer extends EventEmitter {
277307
// const id = Function.prototype.call.call(require, require, "http");
278308
if (this.#neutralCallable.has(identifierName) || isEvilIdentifierPath(fullIdentifierPath)) {
279309
// TODO: make sure we are walking on a require CallExpr here ?
280-
this.#walkRequireCallExpression(variableDeclaratorNode);
310+
this.#walkRequireCallExpression(childNode, id);
281311
}
282312
else if (kUnsafeGlobalCallExpression.has(identifierName)) {
283313
this.#variablesRefToGlobal.add(id.name);
284314
}
285315
// const foo = require("crypto");
286316
// const bar = require.call(null, "crypto");
287317
else if (kRequirePatterns.has(identifierName)) {
288-
this.#walkRequireCallExpression(variableDeclaratorNode);
318+
this.#walkRequireCallExpression(childNode, id);
289319
}
290320
else if (tracedFullIdentifierName === "atob") {
291-
const callExprArguments = getCallExpressionArguments(init, { tracer: this });
321+
const callExprArguments = getCallExpressionArguments(childNode, { tracer: this });
292322
if (callExprArguments === null) {
293323
break;
294324
}
@@ -307,9 +337,9 @@ export class VariableTracer extends EventEmitter {
307337

308338
// const r = require
309339
case "Identifier": {
310-
const identifierName = init.name;
340+
const identifierName = childNode.name;
311341
if (this.#traced.has(identifierName)) {
312-
this.#declareNewAssignment(identifierName, variableDeclaratorNode.id);
342+
this.#declareNewAssignment(identifierName, id);
313343
}
314344
else if (this.#isGlobalVariableIdentifier(identifierName)) {
315345
this.#variablesRefToGlobal.add(id.name);
@@ -321,22 +351,22 @@ export class VariableTracer extends EventEmitter {
321351
// process.mainModule and require.resolve
322352
case "MemberExpression": {
323353
// Example: ["process", "mainModule"]
324-
const memberExprParts = [...getMemberExpressionIdentifier(init, { tracer: this })];
354+
const memberExprParts = [...getMemberExpressionIdentifier(childNode, { tracer: this })];
325355
const memberExprFullname = memberExprParts.join(".");
326356

327357
// Function.prototype.call
328358
if (isNeutralCallable(memberExprFullname)) {
329-
this.#neutralCallable.add(variableDeclaratorNode.id.name);
359+
this.#neutralCallable.add(id.name);
330360
}
331361
else if (this.#traced.has(memberExprFullname)) {
332-
this.#declareNewAssignment(memberExprFullname, variableDeclaratorNode.id);
362+
this.#declareNewAssignment(memberExprFullname, id);
333363
}
334364
else {
335365
const alternativeMemberExprParts = this.#searchForMemberExprAlternative(memberExprParts);
336366
const alternativeMemberExprFullname = alternativeMemberExprParts.join(".");
337367

338368
if (this.#traced.has(alternativeMemberExprFullname)) {
339-
this.#declareNewAssignment(alternativeMemberExprFullname, variableDeclaratorNode.id);
369+
this.#declareNewAssignment(alternativeMemberExprFullname, id);
340370
}
341371
}
342372

@@ -359,14 +389,14 @@ export class VariableTracer extends EventEmitter {
359389

360390
// const {} = Function.prototype.call.call(require, require, "http");
361391
if (isEvilIdentifierPath(fullIdentifierPath)) {
362-
this.#walkRequireCallExpression(variableDeclaratorNode);
392+
this.#walkRequireCallExpression(init, id);
363393
}
364394
else if (kUnsafeGlobalCallExpression.has(identifierName)) {
365395
this.#autoTraceId(id);
366396
}
367397
// const { createHash } = require("crypto");
368398
else if (kRequirePatterns.has(identifierName)) {
369-
this.#walkRequireCallExpression(variableDeclaratorNode);
399+
this.#walkRequireCallExpression(init, id);
370400
}
371401

372402
break;

workspaces/estree-ast-utils/test/VariableTracer/assignments.spec.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,27 @@ test("it should be able to Trace a require Assignment with atob", (tape) => {
131131

132132
tape.end();
133133
});
134+
135+
test("it should be able to Trace a global assignment using a LogicalExpression", (tape) => {
136+
const helpers = createTracer(true);
137+
const assignments = helpers.getAssignmentArray();
138+
139+
helpers.walkOnCode(`
140+
var root = freeGlobal || freeSelf || Function('return this')();
141+
const foo = root.require;
142+
foo("http");
143+
`);
144+
const foo = helpers.tracer.getDataFromIdentifier("foo");
145+
tape.deepEqual(foo, {
146+
name: "require",
147+
identifierOrMemberExpr: "require",
148+
assignmentMemory: ["foo"]
149+
});
150+
tape.strictEqual(assignments.length, 1);
151+
152+
const [eventOne] = assignments;
153+
tape.strictEqual(eventOne.identifierOrMemberExpr, "require");
154+
tape.strictEqual(eventOne.id, "foo");
155+
156+
tape.end();
157+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Import Third-party Dependencies
2+
import test from "tape";
3+
4+
// Import Internal Dependencies
5+
import { extractLogicalExpression } from "../src/index.js";
6+
import { codeToAst, getExpressionFromStatement } from "./utils.js";
7+
8+
test("it should extract two Nodes from a LogicalExpression with two operands", (tape) => {
9+
const [astNode] = codeToAst("5 || 10");
10+
const iter = extractLogicalExpression(
11+
getExpressionFromStatement(astNode)
12+
);
13+
14+
const iterResult = [...iter];
15+
tape.strictEqual(iterResult.length, 2);
16+
17+
tape.strictEqual(iterResult[0].operator, "||");
18+
tape.strictEqual(iterResult[0].node.type, "Literal");
19+
tape.strictEqual(iterResult[0].node.value, 5);
20+
21+
tape.strictEqual(iterResult[1].operator, "||");
22+
tape.strictEqual(iterResult[1].node.type, "Literal");
23+
tape.strictEqual(iterResult[1].node.value, 10);
24+
25+
tape.end();
26+
});
27+
28+
test("it should extract all nodes and add up all Literal values", (tape) => {
29+
const [astNode] = codeToAst("5 || 10 || 15 || 20");
30+
const iter = extractLogicalExpression(
31+
getExpressionFromStatement(astNode)
32+
);
33+
34+
const total = [...iter]
35+
.reduce((previous, { node }) => previous + node.value, 0);
36+
tape.strictEqual(total, 50);
37+
38+
tape.end();
39+
});
40+
41+
test("it should extract all Nodes but with different operators and a LogicalExpr on the right", (tape) => {
42+
const [astNode] = codeToAst("5 || 10 && 55");
43+
const iter = extractLogicalExpression(
44+
getExpressionFromStatement(astNode)
45+
);
46+
47+
const operators = new Set(
48+
[...iter].map(({ operator }) => operator)
49+
);
50+
tape.deepEqual([...operators], ["||", "&&"]);
51+
52+
tape.end();
53+
});

0 commit comments

Comments
 (0)