Skip to content

Commit a92a8df

Browse files
authored
fix(unsafe-import): warning on unsafe-import using eval/require (#190)
* add unsafe-import probe for eval/require * remove unecessary declaration & add eval/require both warning to unsafeimport probe * update test & rename/simmplify new probe * refacto test & rename/validate probe * refacto update isrequire/isunsafecallee, remove new probe * validateNode as array in isRequire probe & update test utils * rebase refacto probe test * update spec, update addDep, clean isRequire & isUnsafeCallee probes * fix init dependencyAutoWarning=false & update docs
1 parent 9a18a14 commit a92a8df

File tree

7 files changed

+89
-15
lines changed

7 files changed

+89
-15
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,4 @@ dist
106106
temp.js
107107
temp/
108108
.vscode/
109+
.idea/

docs/unsafe-import.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ We analyze and trace several ways to require in Node.js (with CJS):
1717
- require.main.require
1818
- require.mainModule.require
1919
- require.resolve
20+
- `const XX = eval('require')('XX');` (dangerous import using eval)
2021

2122
## Example
2223

src/ProbeRunner.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ export class ProbeRunner {
4242
* @type {Probe[]}
4343
*/
4444
static Defaults = [
45+
isRequire,
4546
isUnsafeCallee,
4647
isLiteral,
4748
isLiteralRegex,
4849
isRegexObject,
4950
isVariableDeclaration,
50-
isRequire,
5151
isImportDeclaration,
5252
isMemberExpression,
5353
isAssignmentExpression,

src/SourceFile.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class SourceFile {
2121
inTryStatement = false;
2222
hasDictionaryString = false;
2323
hasPrefixedIdentifiers = false;
24+
dependencyAutoWarning = false;
2425
varkinds = { var: 0, let: 0, const: 0 };
2526
idtypes = { assignExpr: 0, property: 0, variableDeclarator: 0, functionDeclaration: 0 };
2627
counter = {
@@ -52,7 +53,7 @@ export class SourceFile {
5253
}
5354
}
5455

55-
addDependency(name, location = null, unsafe = false) {
56+
addDependency(name, location = null, unsafe = this.dependencyAutoWarning) {
5657
if (typeof name !== "string" || name.trim() === "") {
5758
return;
5859
}
@@ -64,6 +65,10 @@ export class SourceFile {
6465
inTry: this.inTryStatement,
6566
...(location === null ? {} : { location })
6667
});
68+
69+
if (this.dependencyAutoWarning) {
70+
this.addWarning("unsafe-import", dependencyName, location);
71+
}
6772
}
6873

6974
addWarning(name, value, location = rootLocation()) {

src/probes/isRequire.js

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@ import {
1010
getCallExpressionIdentifier,
1111
getCallExpressionArguments
1212
} from "@nodesecure/estree-ast-utils";
13-
14-
// Import Internal Dependencies
1513
import { ProbeSignals } from "../ProbeRunner.js";
1614

17-
function validateNode(node, { tracer }) {
15+
function validateNodeRequire(node, { tracer }) {
1816
const id = getCallExpressionIdentifier(node);
1917
if (id === null) {
2018
return [false];
@@ -24,19 +22,46 @@ function validateNode(node, { tracer }) {
2422

2523
return [
2624
data !== null && data.name === "require",
27-
data?.identifierOrMemberExpr ?? void 0
25+
id ?? void 0
26+
];
27+
}
28+
29+
function validateNodeEvalRequire(node) {
30+
const id = getCallExpressionIdentifier(node);
31+
32+
if (id !== "eval") {
33+
return [false];
34+
}
35+
if (node.callee.type !== "CallExpression") {
36+
return [false];
37+
}
38+
39+
const args = getCallExpressionArguments(node.callee);
40+
41+
return [
42+
args.length > 0 && args.at(0) === "require",
43+
id
2844
];
2945
}
3046

47+
function teardown({ analysis }) {
48+
analysis.dependencyAutoWarning = false;
49+
}
50+
51+
3152
function main(node, options) {
32-
const { analysis } = options;
53+
const { analysis, data: calleeName } = options;
3354
const { tracer } = analysis;
3455

3556
if (node.arguments.length === 0) {
3657
return;
3758
}
3859
const arg = node.arguments.at(0);
3960

61+
if (calleeName === "eval") {
62+
analysis.dependencyAutoWarning = true;
63+
}
64+
4065
switch (arg.type) {
4166
// const foo = "http"; require(foo);
4267
case "Identifier":
@@ -163,7 +188,7 @@ function walkRequireCallExpression(nodeToWalk, tracer) {
163188

164189
export default {
165190
name: "isRequire",
166-
validateNode,
191+
validateNode: [validateNodeRequire, validateNodeEvalRequire],
167192
main,
168193
breakOnMatch: true,
169194
breakGroup: "import"

src/utils.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,30 @@ export function notNullOrUndefined(value) {
77
return value !== null && value !== void 0;
88
}
99

10-
export function isUnsafeCallee(node) {
10+
function isEvalCallee(node) {
11+
const identifier = getCallExpressionIdentifier(node, {
12+
resolveCallExpression: false
13+
});
14+
15+
return identifier === "eval";
16+
}
17+
18+
function isFunctionCallee(node) {
1119
const identifier = getCallExpressionIdentifier(node);
1220

13-
// For Function we are looking for this: `Function("...")();`
14-
// A double CallExpression
15-
return [
16-
identifier === "eval" || (identifier === "Function" && node.callee.type === "CallExpression"),
17-
identifier
18-
];
21+
return identifier === "Function" && node.callee.type === "CallExpression";
22+
}
23+
24+
export function isUnsafeCallee(node) {
25+
if (isEvalCallee(node)) {
26+
return [true, "eval"];
27+
}
28+
29+
if (isFunctionCallee(node)) {
30+
return [true, "Function"];
31+
}
32+
33+
return [false, null];
1934
}
2035

2136
export function rootLocation() {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Import Node.js Dependencies
2+
import { test } from "node:test";
3+
import assert from "node:assert";
4+
5+
// Import Internal Dependencies
6+
import { runASTAnalysis } from "../../index.js";
7+
8+
/**
9+
* @see https://github.com/NodeSecure/js-x-ray/issues/179
10+
*/
11+
// CONSTANTS
12+
const kIncriminedCodeSample = `const stream = eval('require')('stream');`;
13+
const kWarningUnsafeImport = "unsafe-import";
14+
const kWarningUnsafeStatement = "unsafe-stmt";
15+
16+
test("should detect unsafe-import and unsafe-statement", () => {
17+
const sastAnalysis = runASTAnalysis(kIncriminedCodeSample);
18+
19+
assert.equal(sastAnalysis.warnings.at(0).value, "stream");
20+
assert.equal(sastAnalysis.warnings.at(0).kind, kWarningUnsafeImport);
21+
assert.equal(sastAnalysis.warnings.at(1).value, "eval");
22+
assert.equal(sastAnalysis.warnings.at(1).kind, kWarningUnsafeStatement);
23+
assert.equal(sastAnalysis.warnings.length, 2);
24+
assert.equal(sastAnalysis.dependencies.has("stream"), true);
25+
assert.equal(sastAnalysis.dependencies.get("stream").unsafe, true);
26+
assert.equal(sastAnalysis.dependencies.size, 1);
27+
});

0 commit comments

Comments
 (0)