Skip to content

Commit d489d63

Browse files
committed
recognize object transformations in module.exports when looking for library inputs
1 parent 28ad667 commit d489d63

File tree

4 files changed

+99
-2
lines changed

4 files changed

+99
-2
lines changed

javascript/ql/src/semmle/javascript/PackageExports.qll

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ PackageJSON getTopmostPackageJSON() {
4242
* Gets a value exported by the main module from one of the topmost `package.json` files (see `getTopmostPackageJSON`).
4343
* The value is either directly the `module.exports` value, a nested property of `module.exports`, or a method on an exported class.
4444
*/
45-
private DataFlow::Node getAValueExportedByPackage() {
45+
DataFlow::Node getAValueExportedByPackage() {
4646
result = getAnExportFromModule(getTopmostPackageJSON().getMainModule())
4747
or
4848
result = getAValueExportedByPackage().(DataFlow::PropWrite).getRhs()
@@ -70,6 +70,61 @@ private DataFlow::Node getAValueExportedByPackage() {
7070
result = cla.getAStaticMethod() or
7171
result = cla.getConstructor()
7272
)
73+
or
74+
// *****
75+
// Various standard library methods for transforming exported objects.
76+
// *****
77+
//
78+
// Object.defineProperties
79+
exists(DataFlow::MethodCallNode call |
80+
call = DataFlow::globalVarRef("Object").getAMethodCall("defineProperties") and
81+
[call, call.getArgument(0)] = getAValueExportedByPackage() and
82+
result = call.getArgument(any(int i | i > 0))
83+
)
84+
or
85+
// Object.defineProperty
86+
exists(DataFlow::MethodCallNode call |
87+
call = DataFlow::globalVarRef("Object").getAMethodCall("defineProperty") and
88+
[call, call.getArgument(0)] = getAValueExportedByPackage()
89+
|
90+
result = call.getArgument(2).getALocalSource().getAPropertyReference("value")
91+
or
92+
result =
93+
call.getArgument(2)
94+
.getALocalSource()
95+
.getAPropertyReference("get")
96+
.(DataFlow::FunctionNode)
97+
.getAReturn()
98+
)
99+
or
100+
// Object.assign
101+
exists(DataFlow::MethodCallNode assign |
102+
assign = DataFlow::globalVarRef("Object").getAMethodCall("assign")
103+
|
104+
getAValueExportedByPackage() = [assign, assign.getArgument(0)] and
105+
result = assign.getAnArgument()
106+
)
107+
or
108+
// Array.prototype.{map, reduce, entries, values}
109+
exists(DataFlow::MethodCallNode map |
110+
map.getMethodName() = ["map", "reduce", "entries", "values"] and
111+
map = getAValueExportedByPackage()
112+
|
113+
result = map.getArgument(0).getABoundFunctionValue(_).getAReturn()
114+
or
115+
// assuming that the receiver of the call is somehow exported
116+
result = map.getReceiver()
117+
)
118+
or
119+
// Object.{fromEntries, freeze, entries, values}
120+
exists(DataFlow::MethodCallNode freeze |
121+
freeze =
122+
DataFlow::globalVarRef("Object")
123+
.getAMethodCall(["fromEntries", "freeze", "entries", "values"])
124+
|
125+
freeze = getAValueExportedByPackage() and
126+
result = freeze.getArgument(0)
127+
)
73128
}
74129

75130
/**

javascript/ql/src/semmle/javascript/dataflow/TypeTracking.qll

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,12 @@ class TypeBackTracker extends TTypeBackTracker {
252252
*/
253253
predicate start() { hasReturn = false and prop = "" }
254254

255+
/**
256+
* Holds if this is the starting point of type backtracking, and the value is in the property named `propName`.
257+
* The type tracking only ends after the property has been stored.
258+
*/
259+
predicate isInProp(PropertyName propName) { hasReturn = false and prop = propName }
260+
255261
/**
256262
* Holds if this is the end point of type tracking.
257263
*/

javascript/ql/test/query-tests/Security/CWE-078/UnsafeShellCommandConstruction.expected

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ nodes
209209
| lib/lib.js:413:39:413:42 | name |
210210
| lib/lib.js:414:24:414:27 | name |
211211
| lib/lib.js:414:24:414:27 | name |
212+
| lib/lib.js:418:20:418:23 | name |
213+
| lib/lib.js:418:20:418:23 | name |
214+
| lib/lib.js:419:25:419:28 | name |
215+
| lib/lib.js:419:25:419:28 | name |
212216
edges
213217
| lib/lib2.js:3:28:3:31 | name | lib/lib2.js:4:22:4:25 | name |
214218
| lib/lib2.js:3:28:3:31 | name | lib/lib2.js:4:22:4:25 | name |
@@ -452,6 +456,10 @@ edges
452456
| lib/lib.js:413:39:413:42 | name | lib/lib.js:414:24:414:27 | name |
453457
| lib/lib.js:413:39:413:42 | name | lib/lib.js:414:24:414:27 | name |
454458
| lib/lib.js:413:39:413:42 | name | lib/lib.js:414:24:414:27 | name |
459+
| lib/lib.js:418:20:418:23 | name | lib/lib.js:419:25:419:28 | name |
460+
| lib/lib.js:418:20:418:23 | name | lib/lib.js:419:25:419:28 | name |
461+
| lib/lib.js:418:20:418:23 | name | lib/lib.js:419:25:419:28 | name |
462+
| lib/lib.js:418:20:418:23 | name | lib/lib.js:419:25:419:28 | name |
455463
#select
456464
| lib/lib2.js:4:10:4:25 | "rm -rf " + name | lib/lib2.js:3:28:3:31 | name | lib/lib2.js:4:22:4:25 | name | $@ based on library input is later used in $@. | lib/lib2.js:4:10:4:25 | "rm -rf " + name | String concatenation | lib/lib2.js:4:2:4:26 | cp.exec ... + name) | shell command |
457465
| lib/lib2.js:8:10:8:25 | "rm -rf " + name | lib/lib2.js:7:32:7:35 | name | lib/lib2.js:8:22:8:25 | name | $@ based on library input is later used in $@. | lib/lib2.js:8:10:8:25 | "rm -rf " + name | String concatenation | lib/lib2.js:8:2:8:26 | cp.exec ... + name) | shell command |
@@ -511,3 +519,4 @@ edges
511519
| lib/lib.js:366:17:366:56 | "learn ... + model | lib/lib.js:360:20:360:23 | opts | lib/lib.js:366:28:366:42 | this.learn_args | $@ based on library input is later used in $@. | lib/lib.js:366:17:366:56 | "learn ... + model | String concatenation | lib/lib.js:367:3:367:18 | cp.exec(command) | shell command |
512520
| lib/lib.js:406:10:406:25 | "rm -rf " + name | lib/lib.js:405:39:405:42 | name | lib/lib.js:406:22:406:25 | name | $@ based on library input is later used in $@. | lib/lib.js:406:10:406:25 | "rm -rf " + name | String concatenation | lib/lib.js:406:2:406:26 | cp.exec ... + name) | shell command |
513521
| lib/lib.js:414:12:414:27 | "rm -rf " + name | lib/lib.js:413:39:413:42 | name | lib/lib.js:414:24:414:27 | name | $@ based on library input is later used in $@. | lib/lib.js:414:12:414:27 | "rm -rf " + name | String concatenation | lib/lib.js:414:2:414:28 | asyncEx ... + name) | shell command |
522+
| lib/lib.js:419:13:419:28 | "rm -rf " + name | lib/lib.js:418:20:418:23 | name | lib/lib.js:419:25:419:28 | name | $@ based on library input is later used in $@. | lib/lib.js:419:13:419:28 | "rm -rf " + name | String concatenation | lib/lib.js:419:3:419:29 | asyncEx ... + name) | shell command |

javascript/ql/test/query-tests/Security/CWE-078/lib/lib.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,4 +412,31 @@ module.exports.sanitizer3 = function (name) {
412412
var asyncExec = require("async-execute");
413413
module.exports.asyncStuff = function (name) {
414414
asyncExec("rm -rf " + name); // NOT OK
415-
}
415+
}
416+
417+
const myFuncs = {
418+
myFunc: function (name) {
419+
asyncExec("rm -rf " + name); // NOT OK
420+
}
421+
};
422+
423+
module.exports.blabity = {};
424+
425+
Object.defineProperties(
426+
module.exports.blabity,
427+
Object.assign(
428+
{},
429+
Object.entries(myFuncs).reduce(
430+
(props, [ key, value ]) => Object.assign(
431+
props,
432+
{
433+
[key]: {
434+
value,
435+
configurable: true,
436+
},
437+
},
438+
),
439+
{}
440+
)
441+
)
442+
);

0 commit comments

Comments
 (0)