Skip to content

Commit 742abf8

Browse files
committed
refactor package export into a library, and add tests for the library
1 parent d7b852f commit 742abf8

File tree

12 files changed

+149
-66
lines changed

12 files changed

+149
-66
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* EXPERIMENTAL. This API may change in the future.
3+
*
4+
* Provides predicates for working with values exported from a package.
5+
*/
6+
7+
import javascript
8+
9+
/**
10+
* Gets the number of occurrences of "/" in `path`.
11+
*/
12+
bindingset[path]
13+
private int countSlashes(string path) { result = count(path.splitAt("/")) - 1 }
14+
15+
/**
16+
* Gets the topmost package.json that appears in the project.
17+
*
18+
* There can be multiple results if the there exists multiple package.json that are equally deeply nested in the folder structure.
19+
* Results are limited to package.json files that are at most nested 2 directories deep.
20+
*/
21+
PackageJSON getTopmostPackageJSON() {
22+
result =
23+
min(PackageJSON j |
24+
countSlashes(j.getFile().getRelativePath()) <= 3
25+
|
26+
j order by countSlashes(j.getFile().getRelativePath())
27+
)
28+
}
29+
30+
/**
31+
* Gets a value exported by the main module from the package.json `packageJSON`.
32+
* The value is either directly the `module.exports` value, a nested property of `module.exports`, or a method on an exported class.
33+
*/
34+
DataFlow::Node getAValueExportedBy(PackageJSON packageJSON) {
35+
result = getAnExportFromModule(packageJSON.getMainModule())
36+
or
37+
result = getAValueExportedBy(packageJSON).(DataFlow::PropWrite).getRhs()
38+
or
39+
exists(DataFlow::SourceNode callee |
40+
callee = getAValueExportedBy(packageJSON).(DataFlow::NewNode).getCalleeNode().getALocalSource()
41+
|
42+
result = callee.getAPropertyRead("prototype").getAPropertyWrite()
43+
or
44+
result = callee.(DataFlow::ClassNode).getAnInstanceMethod()
45+
)
46+
or
47+
result = getAValueExportedBy(packageJSON).getALocalSource()
48+
or
49+
result = getAValueExportedBy(packageJSON).(DataFlow::SourceNode).getAPropertyReference()
50+
or
51+
exists(Module mod |
52+
mod = getAValueExportedBy(packageJSON).getEnclosingExpr().(Import).getImportedModule()
53+
|
54+
result = getAnExportFromModule(mod)
55+
)
56+
or
57+
exists(DataFlow::ClassNode cla | cla = getAValueExportedBy(packageJSON) |
58+
result = cla.getAnInstanceMethod() or
59+
result = cla.getAStaticMethod() or
60+
result = cla.getConstructor()
61+
)
62+
}
63+
64+
/**
65+
* Gets an exported node from the module `mod`.
66+
*/
67+
private DataFlow::Node getAnExportFromModule(Module mod) {
68+
result.analyze().getAValue() = mod.(NodeModule).getAModuleExportsValue()
69+
or
70+
exists(ASTNode export | result.getEnclosingExpr() = export | mod.exports(_, export))
71+
}

javascript/ql/src/semmle/javascript/security/dataflow/UnsafeShellCommandConstructionCustomizations.qll

Lines changed: 6 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
*/
66

77
import javascript
8-
import semmle.javascript.security.dataflow.RemoteFlowSources
8+
private import semmle.javascript.security.dataflow.RemoteFlowSources
9+
private import semmle.javascript.PackageExports as Exports
910

1011
/**
1112
* Module containing sources, sinks, and sanitizers for shell command constructed from library input.
@@ -44,76 +45,15 @@ module UnsafeShellCommandConstruction {
4445
*/
4546
abstract class Sanitizer extends DataFlow::Node { }
4647

47-
/**
48-
* Gets the number of occurrences of "/" in `path`.
49-
*/
50-
bindingset[path]
51-
private int countSlashes(string path) { result = count(path.splitAt("/")) - 1 }
52-
53-
/**
54-
* Gets the topmost package.json that appears in the project.
55-
*
56-
* There can be multiple results if the there exists multiple package.json that are equally deeply nested in the folder structure.
57-
* Results are limited to package.json files that are at most nested 2 directories deep.
58-
*/
59-
private PackageJSON getTopmostPackageJSON() {
60-
result =
61-
min(PackageJSON j |
62-
countSlashes(j.getFile().getRelativePath()) <= 3
63-
|
64-
j order by countSlashes(j.getFile().getRelativePath())
65-
)
66-
}
67-
68-
/**
69-
* Gets a value exported by the main module from a package.json.
70-
* The value is either directly the `module.exports` value, a nested property of `module.exports`, or a method on an exported class.
71-
*/
72-
private DataFlow::Node getAnExportedValue() {
73-
exists(PackageJSON pack | pack = getTopmostPackageJSON() |
74-
result = getAnExportFromModule(pack.getMainModule())
75-
)
76-
or
77-
result = getAnExportedValue().(DataFlow::PropWrite).getRhs()
78-
or
79-
exists(DataFlow::SourceNode callee |
80-
callee = getAnExportedValue().(DataFlow::NewNode).getCalleeNode().getALocalSource()
81-
|
82-
result = callee.getAPropertyRead("prototype").getAPropertyWrite()
83-
or
84-
result = callee.(DataFlow::ClassNode).getAnInstanceMethod()
85-
)
86-
or
87-
result = getAnExportedValue().getALocalSource()
88-
or
89-
result = getAnExportedValue().(DataFlow::SourceNode).getAPropertyReference()
90-
or
91-
exists(Module mod | mod = getAnExportedValue().getEnclosingExpr().(Import).getImportedModule() |
92-
result = getAnExportFromModule(mod)
93-
)
94-
or
95-
exists(DataFlow::ClassNode cla | cla = getAnExportedValue() |
96-
result = cla.getAnInstanceMethod() or
97-
result = cla.getAStaticMethod() or
98-
result = cla.getConstructor()
99-
)
100-
}
101-
102-
/**
103-
* Gets an exported node from the module `mod`.
104-
*/
105-
private DataFlow::Node getAnExportFromModule(Module mod) {
106-
result.analyze().getAValue() = mod.(NodeModule).getAModuleExportsValue()
107-
or
108-
exists(ASTNode export | result.getEnclosingExpr() = export | mod.exports(_, export))
109-
}
110-
11148
/**
11249
* A parameter of an exported function, seen as a source for shell command constructed from library input.
11350
*/
11451
class ExternalInputSource extends Source, DataFlow::ParameterNode {
11552
ExternalInputSource() {
116-
this = getAnExportedValue().(DataFlow::FunctionNode).getAParameter() and
53+
this =
54+
Exports::getAValueExportedBy(Exports::getTopmostPackageJSON())
55+
.(DataFlow::FunctionNode)
56+
.getAParameter() and
11757
not this.getName() = ["cmd", "command"] // looks to be on purpose.
11858
}
11959
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = function notExporterAnyWhere() {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = function notImportedAnywhere() {}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = function thisIsRequiredFromMain() {}
2+
3+
module.exports.foo = function alsoExported() {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = function alsoNotExported() {}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports = function isExported() {}
2+
3+
module.exports.foo = require("./foo.js")
4+
5+
module.exports.bar = class Bar {
6+
constructor() {} // all are exported
7+
static staticMethod() {}
8+
instanceMethod() {}
9+
}
10+
11+
class Baz {
12+
constructor() {} // not exported
13+
static staticMethod() {} // not exported
14+
instanceMethod() {} // exported
15+
}
16+
17+
module.exports.Baz = new Baz()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"main": "main.js"
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"main": "sublib.js"
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = function exportedInSublibButIsNotAMainPackageExport() {}

0 commit comments

Comments
 (0)