refactor package export into a library, and add tests for the library

This commit is contained in:
Erik Krogh Kristensen
2020-05-18 21:04:24 +02:00
parent d7b852f408
commit 742abf8751
12 changed files with 149 additions and 66 deletions

View File

@@ -0,0 +1,71 @@
/**
* EXPERIMENTAL. This API may change in the future.
*
* Provides predicates for working with values exported from a package.
*/
import javascript
/**
* Gets the number of occurrences of "/" in `path`.
*/
bindingset[path]
private int countSlashes(string path) { result = count(path.splitAt("/")) - 1 }
/**
* Gets the topmost package.json that appears in the project.
*
* There can be multiple results if the there exists multiple package.json that are equally deeply nested in the folder structure.
* Results are limited to package.json files that are at most nested 2 directories deep.
*/
PackageJSON getTopmostPackageJSON() {
result =
min(PackageJSON j |
countSlashes(j.getFile().getRelativePath()) <= 3
|
j order by countSlashes(j.getFile().getRelativePath())
)
}
/**
* Gets a value exported by the main module from the package.json `packageJSON`.
* The value is either directly the `module.exports` value, a nested property of `module.exports`, or a method on an exported class.
*/
DataFlow::Node getAValueExportedBy(PackageJSON packageJSON) {
result = getAnExportFromModule(packageJSON.getMainModule())
or
result = getAValueExportedBy(packageJSON).(DataFlow::PropWrite).getRhs()
or
exists(DataFlow::SourceNode callee |
callee = getAValueExportedBy(packageJSON).(DataFlow::NewNode).getCalleeNode().getALocalSource()
|
result = callee.getAPropertyRead("prototype").getAPropertyWrite()
or
result = callee.(DataFlow::ClassNode).getAnInstanceMethod()
)
or
result = getAValueExportedBy(packageJSON).getALocalSource()
or
result = getAValueExportedBy(packageJSON).(DataFlow::SourceNode).getAPropertyReference()
or
exists(Module mod |
mod = getAValueExportedBy(packageJSON).getEnclosingExpr().(Import).getImportedModule()
|
result = getAnExportFromModule(mod)
)
or
exists(DataFlow::ClassNode cla | cla = getAValueExportedBy(packageJSON) |
result = cla.getAnInstanceMethod() or
result = cla.getAStaticMethod() or
result = cla.getConstructor()
)
}
/**
* Gets an exported node from the module `mod`.
*/
private DataFlow::Node getAnExportFromModule(Module mod) {
result.analyze().getAValue() = mod.(NodeModule).getAModuleExportsValue()
or
exists(ASTNode export | result.getEnclosingExpr() = export | mod.exports(_, export))
}

View File

@@ -5,7 +5,8 @@
*/
import javascript
import semmle.javascript.security.dataflow.RemoteFlowSources
private import semmle.javascript.security.dataflow.RemoteFlowSources
private import semmle.javascript.PackageExports as Exports
/**
* Module containing sources, sinks, and sanitizers for shell command constructed from library input.
@@ -44,76 +45,15 @@ module UnsafeShellCommandConstruction {
*/
abstract class Sanitizer extends DataFlow::Node { }
/**
* Gets the number of occurrences of "/" in `path`.
*/
bindingset[path]
private int countSlashes(string path) { result = count(path.splitAt("/")) - 1 }
/**
* Gets the topmost package.json that appears in the project.
*
* There can be multiple results if the there exists multiple package.json that are equally deeply nested in the folder structure.
* Results are limited to package.json files that are at most nested 2 directories deep.
*/
private PackageJSON getTopmostPackageJSON() {
result =
min(PackageJSON j |
countSlashes(j.getFile().getRelativePath()) <= 3
|
j order by countSlashes(j.getFile().getRelativePath())
)
}
/**
* Gets a value exported by the main module from a package.json.
* The value is either directly the `module.exports` value, a nested property of `module.exports`, or a method on an exported class.
*/
private DataFlow::Node getAnExportedValue() {
exists(PackageJSON pack | pack = getTopmostPackageJSON() |
result = getAnExportFromModule(pack.getMainModule())
)
or
result = getAnExportedValue().(DataFlow::PropWrite).getRhs()
or
exists(DataFlow::SourceNode callee |
callee = getAnExportedValue().(DataFlow::NewNode).getCalleeNode().getALocalSource()
|
result = callee.getAPropertyRead("prototype").getAPropertyWrite()
or
result = callee.(DataFlow::ClassNode).getAnInstanceMethod()
)
or
result = getAnExportedValue().getALocalSource()
or
result = getAnExportedValue().(DataFlow::SourceNode).getAPropertyReference()
or
exists(Module mod | mod = getAnExportedValue().getEnclosingExpr().(Import).getImportedModule() |
result = getAnExportFromModule(mod)
)
or
exists(DataFlow::ClassNode cla | cla = getAnExportedValue() |
result = cla.getAnInstanceMethod() or
result = cla.getAStaticMethod() or
result = cla.getConstructor()
)
}
/**
* Gets an exported node from the module `mod`.
*/
private DataFlow::Node getAnExportFromModule(Module mod) {
result.analyze().getAValue() = mod.(NodeModule).getAModuleExportsValue()
or
exists(ASTNode export | result.getEnclosingExpr() = export | mod.exports(_, export))
}
/**
* A parameter of an exported function, seen as a source for shell command constructed from library input.
*/
class ExternalInputSource extends Source, DataFlow::ParameterNode {
ExternalInputSource() {
this = getAnExportedValue().(DataFlow::FunctionNode).getAParameter() and
this =
Exports::getAValueExportedBy(Exports::getTopmostPackageJSON())
.(DataFlow::FunctionNode)
.getAParameter() and
not this.getName() = ["cmd", "command"] // looks to be on purpose.
}
}

View File

@@ -0,0 +1 @@
module.exports = function notExporterAnyWhere() {}

View File

@@ -0,0 +1 @@
module.exports = function notImportedAnywhere() {}

View File

@@ -0,0 +1,3 @@
module.exports = function thisIsRequiredFromMain() {}
module.exports.foo = function alsoExported() {}

View File

@@ -0,0 +1 @@
module.exports = function alsoNotExported() {}

View File

@@ -0,0 +1,17 @@
module.exports = function isExported() {}
module.exports.foo = require("./foo.js")
module.exports.bar = class Bar {
constructor() {} // all are exported
static staticMethod() {}
instanceMethod() {}
}
class Baz {
constructor() {} // not exported
static staticMethod() {} // not exported
instanceMethod() {} // exported
}
module.exports.Baz = new Baz()

View File

@@ -0,0 +1,3 @@
{
"main": "main.js"
}

View File

@@ -0,0 +1,3 @@
{
"main": "sublib.js"
}

View File

@@ -0,0 +1 @@
module.exports = function exportedInSublibButIsNotAMainPackageExport() {}

View File

@@ -0,0 +1,30 @@
getTopmostPackageJSON
getAValueExportedBy
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/foo.js:1:1:1:0 | this |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/foo.js:1:1:1:53 | module. ... in() {} |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/foo.js:1:18:1:53 | functio ... in() {} |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/foo.js:3:1:3:14 | module.exports |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/foo.js:3:1:3:18 | module.exports.foo |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/foo.js:3:22:3:21 | this |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/foo.js:3:22:3:47 | functio ... ed() {} |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:1:1:1:0 | this |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:1:1:1:41 | module. ... ed() {} |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:1:18:1:41 | functio ... ed() {} |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:3:1:3:14 | module.exports |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:3:1:3:18 | module.exports.foo |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:3:1:3:40 | module. ... oo.js") |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:3:22:3:40 | require("./foo.js") |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:5:1:5:14 | module.exports |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:5:1:5:18 | module.exports.bar |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:5:22:9:1 | class B ... () {}\\n} |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:6:16:6:20 | () {} |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:7:5:7:28 | static ... od() {} |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:7:24:7:28 | () {} |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:8:19:8:23 | () {} |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:14:19:14:23 | () {} |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:17:1:17:14 | module.exports |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:17:1:17:18 | module.exports.Baz |
| lib1/package.json:1:1:3:1 | {\\n " ... n.js"\\n} | lib1/main.js:17:22:17:30 | new Baz() |
| lib1/sublib/package.json:1:1:3:1 | {\\n " ... b.js"\\n} | lib1/sublib/sublib.js:1:1:1:0 | this |
| lib1/sublib/package.json:1:1:3:1 | {\\n " ... b.js"\\n} | lib1/sublib/sublib.js:1:1:1:73 | module. ... rt() {} |
| lib1/sublib/package.json:1:1:3:1 | {\\n " ... b.js"\\n} | lib1/sublib/sublib.js:1:18:1:73 | functio ... rt() {} |

View File

@@ -0,0 +1,12 @@
import javascript
import semmle.javascript.PackageExports as Exports
query PackageJSON getTopmostPackageJSON() {
result = Exports::getTopmostPackageJSON()
}
query DataFlow::Node getAValueExportedBy(PackageJSON json) {
result = Exports::getAValueExportedBy(json)
}