mirror of
https://github.com/github/codeql.git
synced 2026-06-05 21:47:10 +02:00
Compare commits
17 Commits
codeql-cli
...
z80coder/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e63885a06f | ||
|
|
6bd0f1a37d | ||
|
|
cec34d87d2 | ||
|
|
83fe9fb916 | ||
|
|
6ba5f32fd5 | ||
|
|
a69029f99b | ||
|
|
1cabb34e80 | ||
|
|
60b5a453b9 | ||
|
|
3935efc637 | ||
|
|
bfa070090e | ||
|
|
3c23524563 | ||
|
|
b8860a1c93 | ||
|
|
3f4788da96 | ||
|
|
9310a761ad | ||
|
|
9175c2aa11 | ||
|
|
5f6a14b617 | ||
|
|
0d4a9b72a0 |
51
benjamin-button.md
Normal file
51
benjamin-button.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# benjamin-buttons.md
|
||||||
|
|
||||||
|
This file describes the changes that have been applied to
|
||||||
|
the library to make it behave as if it was younger.
|
||||||
|
|
||||||
|
## TaintedPath.ql
|
||||||
|
|
||||||
|
Sinks added between 2020-01-01 and 2020-10-06 have been removed. Found by looking at:
|
||||||
|
|
||||||
|
- the commit titles of https://github.com/github/codeql/commits/main/javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/TaintedPath.expected
|
||||||
|
- the PR titles of https://github.com/github/codeql/pulls?page=2&q=is%3Apr+label%3AJS+is%3Aclosed+sink
|
||||||
|
|
||||||
|
Sinks added between 2018-08-02 and 2020-01-01 have been removed. Found by looking at:
|
||||||
|
|
||||||
|
- the commit titles of https://github.com/github/codeql/commits/main/javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/TaintedPath.expected
|
||||||
|
- the PR titles of https://github.com/github/codeql/pulls?page=2&q=is%3Apr+label%3AJS+is%3Aclosed+sink
|
||||||
|
- the PR titles of https://github.com/github/codeql/pulls?page=2&q=is%3Apr+label%3AJS+is%3Aclosed+pathinjection
|
||||||
|
- the PR titles of https://github.com/github/codeql/pulls?page=2&q=is%3Apr+label%3AJS+is%3Aclosed+tainted-path
|
||||||
|
|
||||||
|
Sinks from the "graceful-fs" and "fs-extra" (added before the open-sourcing squash).
|
||||||
|
|
||||||
|
## Xss.ql
|
||||||
|
|
||||||
|
Sinks added between 2020-01-01 and 2020-10-06 have been removed. Found by looking at:
|
||||||
|
|
||||||
|
- the commit titles of https://github.com/github/codeql/commits/main/javascript/ql/test/query-tests/Security/CWE-079/Xss.expected
|
||||||
|
- the PR titles of https://github.com/github/codeql/pulls?page=2&q=is%3Apr+label%3AJS+is%3Aclosed+sink
|
||||||
|
|
||||||
|
- recursive type tracking for `jQuery::dollar`, `DOM::domValueRef`.
|
||||||
|
|
||||||
|
## SqlInjection.ql
|
||||||
|
|
||||||
|
Sinks added between 2020-01-01 and 2020-10-06 have been removed. Found by looking at:
|
||||||
|
|
||||||
|
- the commit titles of https://github.com/github/codeql/commits/main/javascript/ql/test/query-tests/Security/CWE-089
|
||||||
|
- the PR titles of https://github.com/github/codeql/pulls?page=2&q=is%3Apr+label%3AJS+is%3Aclosed+sink
|
||||||
|
|
||||||
|
Sinks added between 2018-08-02 and 2020-01-01 have been removed. Found by looking at:
|
||||||
|
|
||||||
|
- the commit titles of https://github.com/github/codeql/commits/main/javascript/ql/test/query-tests/Security/CWE-089
|
||||||
|
- the PR titles of https://github.com/github/codeql/pulls?page=2&q=is%3Apr+label%3AJS+is%3Aclosed+sink
|
||||||
|
- the PR titles of https://github.com/github/codeql/pulls?page=2&q=is%3Apr+label%3AJS+is%3Aclosed+sql
|
||||||
|
|
||||||
|
TypeTracking in SQL.qll (added before the open-sourcing squash)
|
||||||
|
|
||||||
|
The model of `mssql` and `sequelize` (added before the open-sourcing squash)
|
||||||
|
|
||||||
|
## PseudoProperties
|
||||||
|
|
||||||
|
Pseudo-properties (`$name$`) used in type-tracking and global dataflow configurations have been disabled.
|
||||||
|
Found by searching for `"\$.*\$"`.
|
||||||
@@ -354,35 +354,6 @@ module DOM {
|
|||||||
call.getNumArgument() = 1 and
|
call.getNumArgument() = 1 and
|
||||||
unique(InferredType t | t = getArgumentTypeFromJQueryMethodGet(call)) = TTNumber()
|
unique(InferredType t | t = getArgumentTypeFromJQueryMethodGet(call)) = TTNumber()
|
||||||
)
|
)
|
||||||
or
|
|
||||||
// A `this` node from a callback given to a `$().each(callback)` call.
|
|
||||||
// purposely not using JQuery::MethodCall to avoid `jquery.each()`.
|
|
||||||
exists(DataFlow::CallNode eachCall | eachCall = JQuery::objectRef().getAMethodCall("each") |
|
|
||||||
this = DataFlow::thisNode(eachCall.getCallback(0).getFunction()) or
|
|
||||||
this = eachCall.getABoundCallbackParameter(0, 1)
|
|
||||||
)
|
|
||||||
or
|
|
||||||
// A read of an array-element from a JQuery object. E.g. `$("#foo")[0]`
|
|
||||||
exists(DataFlow::PropRead read |
|
|
||||||
read = this and read = JQuery::objectRef().getAPropertyRead()
|
|
||||||
|
|
|
||||||
unique(InferredType t | t = read.getPropertyNameExpr().analyze().getAType()) = TTNumber()
|
|
||||||
)
|
|
||||||
or
|
|
||||||
// A receiver node of an event handler on a DOM node
|
|
||||||
exists(DataFlow::SourceNode domNode, DataFlow::FunctionNode eventHandler |
|
|
||||||
// NOTE: we do not use `getABoundFunctionValue()`, since bound functions tend to have
|
|
||||||
// a different receiver anyway
|
|
||||||
eventHandler = domNode.getAPropertySource(any(string n | n.matches("on%")))
|
|
||||||
or
|
|
||||||
eventHandler =
|
|
||||||
domNode.getAMethodCall("addEventListener").getArgument(1).getAFunctionValue()
|
|
||||||
|
|
|
||||||
domNode = domValueRef() and
|
|
||||||
this = eventHandler.getReceiver()
|
|
||||||
)
|
|
||||||
or
|
|
||||||
this = DataFlow::thisNode(any(EventHandlerCode evt))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,11 +387,6 @@ module DOM {
|
|||||||
or
|
or
|
||||||
t.start() and
|
t.start() and
|
||||||
result = domValueRef().getAMethodCall(["item", "namedItem"])
|
result = domValueRef().getAMethodCall(["item", "namedItem"])
|
||||||
or
|
|
||||||
t.startInProp("target") and
|
|
||||||
result = domEventSource()
|
|
||||||
or
|
|
||||||
exists(DataFlow::TypeTracker t2 | result = domValueRef(t2).track(t2, t))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets a data flow node that may refer to a value from the DOM. */
|
/** Gets a data flow node that may refer to a value from the DOM. */
|
||||||
|
|||||||
@@ -183,12 +183,12 @@ module Promises {
|
|||||||
/**
|
/**
|
||||||
* Gets the pseudo-field used to describe resolved values in a promise.
|
* Gets the pseudo-field used to describe resolved values in a promise.
|
||||||
*/
|
*/
|
||||||
string valueProp() { result = "$PromiseResolveField$" }
|
string valueProp() { none() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the pseudo-field used to describe rejected values in a promise.
|
* Gets the pseudo-field used to describe rejected values in a promise.
|
||||||
*/
|
*/
|
||||||
string errorProp() { result = "$PromiseRejectField$" }
|
string errorProp() { none() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -777,10 +777,10 @@ private class AdditionalFlowStepAsSharedStep extends SharedFlowStep {
|
|||||||
*/
|
*/
|
||||||
module PseudoProperties {
|
module PseudoProperties {
|
||||||
bindingset[s]
|
bindingset[s]
|
||||||
private string pseudoProperty(string s) { result = "$" + s + "$" }
|
private string pseudoProperty(string s) { none() }
|
||||||
|
|
||||||
bindingset[s, v]
|
bindingset[s, v]
|
||||||
private string pseudoProperty(string s, string v) { result = "$" + s + "|" + v + "$" }
|
private string pseudoProperty(string s, string v) { none() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a pseudo-property for the location of elements in a `Set`
|
* Gets a pseudo-property for the location of elements in a `Set`
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ module Angular2 {
|
|||||||
|
|
||||||
/** Gets a reference to a `DomSanitizer` object. */
|
/** Gets a reference to a `DomSanitizer` object. */
|
||||||
DataFlow::SourceNode domSanitizer() {
|
DataFlow::SourceNode domSanitizer() {
|
||||||
result.hasUnderlyingType(["@angular/platform-browser", "@angular/core"], "DomSanitizer")
|
result.hasUnderlyingType("@angular/platform-browser", "DomSanitizer")
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A value that is about to be promoted to a trusted HTML or CSS value. */
|
/** A value that is about to be promoted to a trusted HTML or CSS value. */
|
||||||
|
|||||||
@@ -927,28 +927,6 @@ module Express {
|
|||||||
override string getCredentialsKind() { result = kind }
|
override string getCredentialsKind() { result = kind }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A call to `response.sendFile`, considered as a file system access. */
|
|
||||||
private class ResponseSendFileAsFileSystemAccess extends FileSystemReadAccess,
|
|
||||||
DataFlow::MethodCallNode {
|
|
||||||
ResponseSendFileAsFileSystemAccess() {
|
|
||||||
exists(string name | name = "sendFile" or name = "sendfile" |
|
|
||||||
this.calls(any(ResponseExpr res).flow(), name)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getADataNode() { none() }
|
|
||||||
|
|
||||||
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
|
|
||||||
|
|
||||||
override DataFlow::Node getRootPathArgument() {
|
|
||||||
result = this.(DataFlow::CallNode).getOptionArgument(1, "root")
|
|
||||||
}
|
|
||||||
|
|
||||||
override predicate isUpwardNavigationRejected(DataFlow::Node argument) {
|
|
||||||
argument = this.getAPathArgument()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function that flows to a route setup.
|
* A function that flows to a route setup.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,23 +4,6 @@
|
|||||||
|
|
||||||
import javascript
|
import javascript
|
||||||
|
|
||||||
/**
|
|
||||||
* A call that can produce a file name.
|
|
||||||
*/
|
|
||||||
abstract private class FileNameProducer extends DataFlow::Node {
|
|
||||||
/**
|
|
||||||
* Gets a file name produced by this producer.
|
|
||||||
*/
|
|
||||||
abstract DataFlow::Node getAFileName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A node that contains a file name, and is produced by a `ProducesFileNames`.
|
|
||||||
*/
|
|
||||||
private class ProducedFileName extends FileNameSource {
|
|
||||||
ProducedFileName() { this = any(FileNameProducer producer).getAFileName() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A file name from the `walk-sync` library.
|
* A file name from the `walk-sync` library.
|
||||||
*/
|
*/
|
||||||
@@ -143,341 +126,3 @@ private DataFlow::Node fastGlobFileNameSource(DataFlow::TypeTracker t) {
|
|||||||
private class FastGlobFileNameSource extends FileNameSource {
|
private class FastGlobFileNameSource extends FileNameSource {
|
||||||
FastGlobFileNameSource() { this = fastGlobFileNameSource(DataFlow::TypeTracker::end()) }
|
FastGlobFileNameSource() { this = fastGlobFileNameSource(DataFlow::TypeTracker::end()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Classes and predicates for modeling the `fstream` library (https://www.npmjs.com/package/fstream).
|
|
||||||
*/
|
|
||||||
private module FStream {
|
|
||||||
/**
|
|
||||||
* Gets a reference to a method in the `fstream` library.
|
|
||||||
*/
|
|
||||||
private DataFlow::SourceNode getAnFStreamProperty(boolean writer) {
|
|
||||||
exists(DataFlow::SourceNode mod, string readOrWrite, string subMod |
|
|
||||||
mod = DataFlow::moduleImport("fstream") and
|
|
||||||
(
|
|
||||||
readOrWrite = "Reader" and writer = false
|
|
||||||
or
|
|
||||||
readOrWrite = "Writer" and writer = true
|
|
||||||
) and
|
|
||||||
subMod = ["File", "Dir", "Link", "Proxy"]
|
|
||||||
|
|
|
||||||
result = mod.getAPropertyRead(readOrWrite) or
|
|
||||||
result = mod.getAPropertyRead(readOrWrite).getAPropertyRead(subMod) or
|
|
||||||
result = mod.getAPropertyRead(subMod).getAPropertyRead(readOrWrite)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An invocation of a method defined in the `fstream` library.
|
|
||||||
*/
|
|
||||||
private class FStream extends FileSystemAccess, DataFlow::InvokeNode {
|
|
||||||
boolean writer;
|
|
||||||
|
|
||||||
FStream() { this = getAnFStreamProperty(writer).getAnInvocation() }
|
|
||||||
|
|
||||||
override DataFlow::Node getAPathArgument() {
|
|
||||||
result = this.getOptionArgument(0, "path")
|
|
||||||
or
|
|
||||||
not exists(this.getOptionArgument(0, "path")) and
|
|
||||||
result = this.getArgument(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An invocation of an `fstream` method that writes to a file.
|
|
||||||
*/
|
|
||||||
private class FStreamWriter extends FileSystemWriteAccess, FStream {
|
|
||||||
FStreamWriter() { writer = true }
|
|
||||||
|
|
||||||
override DataFlow::Node getADataNode() { none() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An invocation of an `fstream` method that reads a file.
|
|
||||||
*/
|
|
||||||
private class FStreamReader extends FileSystemReadAccess, FStream {
|
|
||||||
FStreamReader() { writer = false }
|
|
||||||
|
|
||||||
override DataFlow::Node getADataNode() { none() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A call to the library `write-file-atomic`.
|
|
||||||
*/
|
|
||||||
private class WriteFileAtomic extends FileSystemWriteAccess, DataFlow::CallNode {
|
|
||||||
WriteFileAtomic() {
|
|
||||||
this = DataFlow::moduleImport("write-file-atomic").getACall()
|
|
||||||
or
|
|
||||||
this = DataFlow::moduleMember("write-file-atomic", "sync").getACall()
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
|
|
||||||
|
|
||||||
override DataFlow::Node getADataNode() { result = this.getArgument(1) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A call to the library `recursive-readdir`.
|
|
||||||
*/
|
|
||||||
private class RecursiveReadDir extends FileSystemAccess, FileNameProducer, DataFlow::CallNode {
|
|
||||||
RecursiveReadDir() { this = DataFlow::moduleImport("recursive-readdir").getACall() }
|
|
||||||
|
|
||||||
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
|
|
||||||
|
|
||||||
override DataFlow::Node getAFileName() {
|
|
||||||
result = this.trackFileSource(DataFlow::TypeTracker::end())
|
|
||||||
}
|
|
||||||
|
|
||||||
private DataFlow::SourceNode trackFileSource(DataFlow::TypeTracker t) {
|
|
||||||
t.start() and result = this.getCallback([1 .. 2]).getParameter(1)
|
|
||||||
or
|
|
||||||
t.startInPromise() and not exists(this.getCallback([1 .. 2])) and result = this
|
|
||||||
or
|
|
||||||
// Tracking out of a promise
|
|
||||||
exists(DataFlow::TypeTracker t2 |
|
|
||||||
result = PromiseTypeTracking::promiseStep(this.trackFileSource(t2), t, t2)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Classes and predicates for modeling the `jsonfile` library (https://www.npmjs.com/package/jsonfile).
|
|
||||||
*/
|
|
||||||
private module JSONFile {
|
|
||||||
/**
|
|
||||||
* A reader for JSON files.
|
|
||||||
*/
|
|
||||||
class JSONFileReader extends FileSystemReadAccess, DataFlow::CallNode {
|
|
||||||
JSONFileReader() {
|
|
||||||
this =
|
|
||||||
DataFlow::moduleMember("jsonfile", any(string s | s = "readFile" or s = "readFileSync"))
|
|
||||||
.getACall()
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
|
|
||||||
|
|
||||||
override DataFlow::Node getADataNode() { result = this.trackRead(DataFlow::TypeTracker::end()) }
|
|
||||||
|
|
||||||
private DataFlow::SourceNode trackRead(DataFlow::TypeTracker t) {
|
|
||||||
this.getCalleeName() = "readFile" and
|
|
||||||
(
|
|
||||||
t.start() and result = this.getCallback([1 .. 2]).getParameter(1)
|
|
||||||
or
|
|
||||||
t.startInPromise() and not exists(this.getCallback([1 .. 2])) and result = this
|
|
||||||
)
|
|
||||||
or
|
|
||||||
t.start() and
|
|
||||||
this.getCalleeName() = "readFileSync" and
|
|
||||||
result = this
|
|
||||||
or
|
|
||||||
// Tracking out of a promise
|
|
||||||
exists(DataFlow::TypeTracker t2 |
|
|
||||||
result = PromiseTypeTracking::promiseStep(this.trackRead(t2), t, t2)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A writer for JSON files.
|
|
||||||
*/
|
|
||||||
class JSONFileWriter extends FileSystemWriteAccess, DataFlow::CallNode {
|
|
||||||
JSONFileWriter() {
|
|
||||||
this =
|
|
||||||
DataFlow::moduleMember("jsonfile", any(string s | s = "writeFile" or s = "writeFileSync"))
|
|
||||||
.getACall()
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
|
|
||||||
|
|
||||||
override DataFlow::Node getADataNode() { result = this.getArgument(1) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A call to the library `load-json-file`.
|
|
||||||
*/
|
|
||||||
private class LoadJsonFile extends FileSystemReadAccess, DataFlow::CallNode {
|
|
||||||
LoadJsonFile() {
|
|
||||||
this = DataFlow::moduleImport("load-json-file").getACall()
|
|
||||||
or
|
|
||||||
this = DataFlow::moduleMember("load-json-file", "sync").getACall()
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
|
|
||||||
|
|
||||||
override DataFlow::Node getADataNode() { result = this.trackRead(DataFlow::TypeTracker::end()) }
|
|
||||||
|
|
||||||
private DataFlow::SourceNode trackRead(DataFlow::TypeTracker t) {
|
|
||||||
this.getCalleeName() = "sync" and t.start() and result = this
|
|
||||||
or
|
|
||||||
not this.getCalleeName() = "sync" and t.startInPromise() and result = this
|
|
||||||
or
|
|
||||||
// Tracking out of a promise
|
|
||||||
exists(DataFlow::TypeTracker t2 |
|
|
||||||
result = PromiseTypeTracking::promiseStep(this.trackRead(t2), t, t2)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A call to the library `write-json-file`.
|
|
||||||
*/
|
|
||||||
private class WriteJsonFile extends FileSystemWriteAccess, DataFlow::CallNode {
|
|
||||||
WriteJsonFile() {
|
|
||||||
this = DataFlow::moduleImport("write-json-file").getACall()
|
|
||||||
or
|
|
||||||
this = DataFlow::moduleMember("write-json-file", "sync").getACall()
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
|
|
||||||
|
|
||||||
override DataFlow::Node getADataNode() { result = this.getArgument(1) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A call to the library `walkdir`.
|
|
||||||
*/
|
|
||||||
private class WalkDir extends FileNameProducer, FileSystemAccess, DataFlow::CallNode {
|
|
||||||
WalkDir() {
|
|
||||||
this = DataFlow::moduleImport("walkdir").getACall()
|
|
||||||
or
|
|
||||||
this = DataFlow::moduleMember("walkdir", "sync").getACall()
|
|
||||||
or
|
|
||||||
this = DataFlow::moduleMember("walkdir", "async").getACall()
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
|
|
||||||
|
|
||||||
override DataFlow::Node getAFileName() {
|
|
||||||
result = this.trackFileSource(DataFlow::TypeTracker::end())
|
|
||||||
}
|
|
||||||
|
|
||||||
private DataFlow::SourceNode trackFileSource(DataFlow::TypeTracker t) {
|
|
||||||
not this.getCalleeName() = any(string s | s = "sync" or s = "async") and
|
|
||||||
t.start() and
|
|
||||||
(
|
|
||||||
result = this.getCallback(this.getNumArgument() - 1).getParameter(0)
|
|
||||||
or
|
|
||||||
result = this.getAMethodCall(EventEmitter::on()).getCallback(1).getParameter(0)
|
|
||||||
)
|
|
||||||
or
|
|
||||||
t.start() and this.getCalleeName() = "sync" and result = this
|
|
||||||
or
|
|
||||||
t.startInPromise() and this.getCalleeName() = "async" and result = this
|
|
||||||
or
|
|
||||||
// Tracking out of a promise
|
|
||||||
exists(DataFlow::TypeTracker t2 |
|
|
||||||
result = PromiseTypeTracking::promiseStep(this.trackFileSource(t2), t, t2)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A call to the library `globule`.
|
|
||||||
*/
|
|
||||||
private class Globule extends FileNameProducer, FileSystemAccess, DataFlow::CallNode {
|
|
||||||
Globule() {
|
|
||||||
this = DataFlow::moduleMember("globule", "find").getACall()
|
|
||||||
or
|
|
||||||
this = DataFlow::moduleMember("globule", "match").getACall()
|
|
||||||
or
|
|
||||||
this = DataFlow::moduleMember("globule", "isMatch").getACall()
|
|
||||||
or
|
|
||||||
this = DataFlow::moduleMember("globule", "mapping").getACall()
|
|
||||||
or
|
|
||||||
this = DataFlow::moduleMember("globule", "findMapping").getACall()
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAPathArgument() {
|
|
||||||
(this.getCalleeName() = "match" or this.getCalleeName() = "isMatch") and
|
|
||||||
result = this.getArgument(1)
|
|
||||||
or
|
|
||||||
this.getCalleeName() = "mapping" and
|
|
||||||
(
|
|
||||||
result = this.getAnArgument() and
|
|
||||||
not exists(result.getALocalSource().getAPropertyWrite("src"))
|
|
||||||
or
|
|
||||||
result = this.getAnArgument().getALocalSource().getAPropertyWrite("src").getRhs()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAFileName() {
|
|
||||||
result = this and
|
|
||||||
(
|
|
||||||
this.getCalleeName() = "find" or
|
|
||||||
this.getCalleeName() = "match" or
|
|
||||||
this.getCalleeName() = "findMapping" or
|
|
||||||
this.getCalleeName() = "mapping"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A file system access made by a NodeJS library.
|
|
||||||
* This class models multiple NodeJS libraries that access files.
|
|
||||||
*/
|
|
||||||
private class LibraryAccess extends FileSystemAccess, DataFlow::InvokeNode {
|
|
||||||
int pathArgument; // The index of the path argument.
|
|
||||||
|
|
||||||
LibraryAccess() {
|
|
||||||
pathArgument = 0 and
|
|
||||||
(
|
|
||||||
this = DataFlow::moduleImport("path-exists").getACall()
|
|
||||||
or
|
|
||||||
this = DataFlow::moduleImport("rimraf").getACall()
|
|
||||||
or
|
|
||||||
this = DataFlow::moduleImport("readdirp").getACall()
|
|
||||||
or
|
|
||||||
this = DataFlow::moduleImport("walker").getACall()
|
|
||||||
or
|
|
||||||
this =
|
|
||||||
DataFlow::moduleMember("node-dir",
|
|
||||||
["readFiles", "readFilesStream", "files", "promiseFiles", "subdirs", "paths"]).getACall()
|
|
||||||
)
|
|
||||||
or
|
|
||||||
pathArgument = 0 and
|
|
||||||
this =
|
|
||||||
DataFlow::moduleMember("vinyl-fs", any(string s | s = "src" or s = "dest" or s = "symlink"))
|
|
||||||
.getACall()
|
|
||||||
or
|
|
||||||
pathArgument = [0 .. 1] and
|
|
||||||
(
|
|
||||||
this = DataFlow::moduleImport("ncp").getACall() or
|
|
||||||
this = DataFlow::moduleMember("ncp", "ncp").getACall()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAPathArgument() { result = this.getArgument(pathArgument) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A call to the library [`chokidar`](https://www.npmjs.com/package/chokidar), where a call to `on` receives file names.
|
|
||||||
*/
|
|
||||||
class Chokidar extends FileNameProducer, FileSystemAccess, API::CallNode {
|
|
||||||
Chokidar() { this = API::moduleImport("chokidar").getMember("watch").getACall() }
|
|
||||||
|
|
||||||
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
|
|
||||||
|
|
||||||
override DataFlow::Node getAFileName() {
|
|
||||||
exists(DataFlow::CallNode onCall, int pathIndex |
|
|
||||||
onCall = this.getAChainedMethodCall("on") and
|
|
||||||
if onCall.getArgument(0).mayHaveStringValue("all") then pathIndex = 1 else pathIndex = 0
|
|
||||||
|
|
|
||||||
result = onCall.getCallback(1).getParameter(pathIndex)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A call to the [`mkdirp`](https://www.npmjs.com/package/mkdirp) library.
|
|
||||||
*/
|
|
||||||
private class Mkdirp extends FileSystemAccess, API::CallNode {
|
|
||||||
Mkdirp() {
|
|
||||||
this = API::moduleImport("mkdirp").getACall()
|
|
||||||
or
|
|
||||||
this = API::moduleImport("mkdirp").getMember("sync").getACall()
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,17 +7,7 @@ import semmle.javascript.Promises
|
|||||||
|
|
||||||
module NoSQL {
|
module NoSQL {
|
||||||
/** An expression that is interpreted as a NoSQL query. */
|
/** An expression that is interpreted as a NoSQL query. */
|
||||||
abstract class Query extends Expr {
|
abstract class Query extends Expr { }
|
||||||
/** Gets an expression that is interpreted as a code operator in this query. */
|
|
||||||
DataFlow::Node getACodeOperator() { none() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a value that has been assigned to the "$where" property of an object that flows to `queryArg`.
|
|
||||||
*/
|
|
||||||
private DataFlow::Node getADollarWhereProperty(API::Node queryArg) {
|
|
||||||
result = queryArg.getMember("$where").getARhs()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,123 +15,112 @@ private DataFlow::Node getADollarWhereProperty(API::Node queryArg) {
|
|||||||
*/
|
*/
|
||||||
private module MongoDB {
|
private module MongoDB {
|
||||||
/**
|
/**
|
||||||
* Gets an access to `mongodb.MongoClient` or a database.
|
* Gets an import of MongoDB.
|
||||||
*
|
|
||||||
* In Mongo version 2.x, a client and a database handle were the same concept, but in 3.x
|
|
||||||
* they were separated. To handle everything with a single model, we treat them as the same here.
|
|
||||||
*/
|
*/
|
||||||
private API::Node getAMongoClientOrDatabase() {
|
DataFlow::ModuleImportNode mongodb() { result.getPath() = "mongodb" }
|
||||||
result = API::moduleImport("mongodb").getMember("MongoClient")
|
|
||||||
|
/**
|
||||||
|
* Gets an access to `mongodb.MongoClient`.
|
||||||
|
*/
|
||||||
|
private DataFlow::SourceNode getAMongoClient(DataFlow::TypeTracker t) {
|
||||||
|
t.start() and
|
||||||
|
result = mongodb().getAPropertyRead("MongoClient")
|
||||||
or
|
or
|
||||||
result = getAMongoClientOrDatabase().getMember("db").getReturn()
|
exists(DataFlow::TypeTracker t2 | result = getAMongoClient(t2).track(t2, t))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an access to `mongodb.MongoClient`.
|
||||||
|
*/
|
||||||
|
DataFlow::SourceNode getAMongoClient() { result = getAMongoClient(DataFlow::TypeTracker::end()) }
|
||||||
|
|
||||||
|
/** Gets a data flow node that leads to a `connect` callback. */
|
||||||
|
private DataFlow::SourceNode getAMongoDbCallback(DataFlow::TypeBackTracker t) {
|
||||||
|
t.start() and
|
||||||
|
result = getAMongoClient().getAMemberCall("connect").getArgument(1).getALocalSource()
|
||||||
or
|
or
|
||||||
result = getAMongoClientOrDatabase().getMember("connect").getLastParameter().getParameter(1)
|
exists(DataFlow::TypeBackTracker t2 | result = getAMongoDbCallback(t2).backtrack(t2, t))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets a data flow node that leads to a `connect` callback. */
|
||||||
|
private DataFlow::FunctionNode getAMongoDbCallback() {
|
||||||
|
result = getAMongoDbCallback(DataFlow::TypeBackTracker::end())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an expression that may refer to a MongoDB database connection.
|
||||||
|
*/
|
||||||
|
private DataFlow::SourceNode getAMongoDb(DataFlow::TypeTracker t) {
|
||||||
|
t.start() and
|
||||||
|
result = getAMongoDbCallback().getParameter(1)
|
||||||
|
or
|
||||||
|
exists(DataFlow::TypeTracker t2 | result = getAMongoDb(t2).track(t2, t))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an expression that may refer to a MongoDB database connection.
|
||||||
|
*/
|
||||||
|
DataFlow::SourceNode getAMongoDb() { result = getAMongoDb(DataFlow::TypeTracker::end()) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data flow node that may hold a MongoDB collection.
|
||||||
|
*/
|
||||||
|
abstract class Collection extends DataFlow::SourceNode { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection resulting from calling `Db.collection(...)`.
|
||||||
|
*/
|
||||||
|
private class CollectionFromDb extends Collection {
|
||||||
|
CollectionFromDb() {
|
||||||
|
this = getAMongoDb().getAMethodCall("collection")
|
||||||
|
or
|
||||||
|
this = getAMongoDb().getAMethodCall("collection").getCallback(1).getParameter(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection based on the type `mongodb.Collection`.
|
||||||
|
*
|
||||||
|
* Note that this also covers `mongoose` models since they are subtypes
|
||||||
|
* of `mongodb.Collection`.
|
||||||
|
*/
|
||||||
|
private class CollectionFromType extends Collection {
|
||||||
|
CollectionFromType() { hasUnderlyingType("mongodb", "Collection") }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets a data flow node referring to a MongoDB collection. */
|
/** Gets a data flow node referring to a MongoDB collection. */
|
||||||
private API::Node getACollection() {
|
private DataFlow::SourceNode getACollection(DataFlow::TypeTracker t) {
|
||||||
// A collection resulting from calling `Db.collection(...)`.
|
t.start() and
|
||||||
exists(API::Node collection |
|
result instanceof Collection
|
||||||
collection = getAMongoClientOrDatabase().getMember("collection").getReturn()
|
|
||||||
|
|
|
||||||
result = collection
|
|
||||||
or
|
|
||||||
result = collection.getParameter(1).getParameter(0)
|
|
||||||
)
|
|
||||||
or
|
or
|
||||||
// note that this also covers `mongoose` models since they are subtypes of `mongodb.Collection`
|
exists(DataFlow::TypeTracker t2 | result = getACollection(t2).track(t2, t))
|
||||||
result = API::Node::ofType("mongodb", "Collection")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets a data flow node referring to a MongoDB collection. */
|
||||||
|
DataFlow::SourceNode getACollection() { result = getACollection(DataFlow::TypeTracker::end()) }
|
||||||
|
|
||||||
/** A call to a MongoDB query method. */
|
/** A call to a MongoDB query method. */
|
||||||
private class QueryCall extends DatabaseAccess, API::CallNode {
|
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
|
||||||
int queryArgIdx;
|
int queryArgIdx;
|
||||||
|
|
||||||
QueryCall() {
|
QueryCall() {
|
||||||
exists(string method |
|
exists(string m | this = getACollection().getAMethodCall(m) |
|
||||||
CollectionMethodSignatures::interpretsArgumentAsQuery(method, queryArgIdx) and
|
m = "count" and queryArgIdx = 0
|
||||||
this = getACollection().getMember(method).getACall()
|
or
|
||||||
|
m = "distinct" and queryArgIdx = 1
|
||||||
|
or
|
||||||
|
m = "find" and queryArgIdx = 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override DataFlow::Node getAQueryArgument() { result = this.getArgument(queryArgIdx) }
|
override DataFlow::Node getAQueryArgument() { result = getArgument(queryArgIdx) }
|
||||||
|
|
||||||
override DataFlow::Node getAResult() {
|
|
||||||
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
|
|
||||||
}
|
|
||||||
|
|
||||||
DataFlow::Node getACodeOperator() {
|
|
||||||
result = getADollarWhereProperty(this.getParameter(queryArgIdx))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An expression that is interpreted as a MongoDB query.
|
* An expression that is interpreted as a MongoDB query.
|
||||||
*/
|
*/
|
||||||
class Query extends NoSQL::Query {
|
class Query extends NoSQL::Query {
|
||||||
QueryCall qc;
|
Query() { this = any(QueryCall qc).getAQueryArgument().asExpr() }
|
||||||
|
|
||||||
Query() { this = qc.getAQueryArgument().asExpr() }
|
|
||||||
|
|
||||||
override DataFlow::Node getACodeOperator() { result = qc.getACodeOperator() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides signatures for the Collection methods.
|
|
||||||
*/
|
|
||||||
module CollectionMethodSignatures {
|
|
||||||
/**
|
|
||||||
* Holds if Collection method `name` interprets parameter `n` as a query.
|
|
||||||
*/
|
|
||||||
predicate interpretsArgumentAsQuery(string name, int n) {
|
|
||||||
// FilterQuery
|
|
||||||
(
|
|
||||||
name = "aggregate" and n = 0
|
|
||||||
or
|
|
||||||
name = "count" and n = 0
|
|
||||||
or
|
|
||||||
name = "countDocuments" and n = 0
|
|
||||||
or
|
|
||||||
name = "deleteMany" and n = 0
|
|
||||||
or
|
|
||||||
name = "deleteOne" and n = 0
|
|
||||||
or
|
|
||||||
name = "distinct" and n = 1
|
|
||||||
or
|
|
||||||
name = "find" and n = 0
|
|
||||||
or
|
|
||||||
name = "findOne" and n = 0
|
|
||||||
or
|
|
||||||
name = "findOneAndDelete" and n = 0
|
|
||||||
or
|
|
||||||
name = "findOneAndRemove" and n = 0
|
|
||||||
or
|
|
||||||
name = "findOneAndReplace" and n = 0
|
|
||||||
or
|
|
||||||
name = "findOneAndUpdate" and n = 0
|
|
||||||
or
|
|
||||||
name = "remove" and n = 0
|
|
||||||
or
|
|
||||||
name = "replaceOne" and n = 0
|
|
||||||
or
|
|
||||||
name = "update" and n = 0
|
|
||||||
or
|
|
||||||
name = "updateMany" and n = 0
|
|
||||||
or
|
|
||||||
name = "updateOne" and n = 0
|
|
||||||
)
|
|
||||||
or
|
|
||||||
// UpdateQuery
|
|
||||||
(
|
|
||||||
name = "findOneAndUpdate" and n = 1
|
|
||||||
or
|
|
||||||
name = "update" and n = 1
|
|
||||||
or
|
|
||||||
name = "updateMany" and n = 1
|
|
||||||
or
|
|
||||||
name = "updateOne" and n = 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,354 +131,33 @@ private module Mongoose {
|
|||||||
/**
|
/**
|
||||||
* Gets an import of Mongoose.
|
* Gets an import of Mongoose.
|
||||||
*/
|
*/
|
||||||
API::Node getAMongooseInstance() { result = API::moduleImport("mongoose") }
|
DataFlow::ModuleImportNode getAMongooseInstance() { result.getPath() = "mongoose" }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a reference to `mongoose.createConnection`.
|
* Gets a call to `mongoose.createConnection`.
|
||||||
*/
|
*/
|
||||||
API::Node createConnection() { result = getAMongooseInstance().getMember("createConnection") }
|
DataFlow::CallNode createConnection() {
|
||||||
|
result = getAMongooseInstance().getAMemberCall("createConnection")
|
||||||
/**
|
|
||||||
* A Mongoose function.
|
|
||||||
*/
|
|
||||||
abstract private class MongooseFunction extends API::Node {
|
|
||||||
/**
|
|
||||||
* Gets the API-graph node for the result from this function (if the function returns a `Query`).
|
|
||||||
*/
|
|
||||||
abstract API::Node getQueryReturn();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds if this function returns a `Query` that evaluates to one or
|
|
||||||
* more Documents (`asArray` is false if it evaluates to a single
|
|
||||||
* Document).
|
|
||||||
*/
|
|
||||||
abstract predicate returnsDocumentQuery(boolean asArray);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets an argument that this function interprets as a query.
|
|
||||||
*/
|
|
||||||
abstract API::Node getQueryArgument();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides classes modeling the Mongoose Model class
|
* A Mongoose collection object.
|
||||||
*/
|
*/
|
||||||
module Model {
|
class Model extends MongoDB::Collection {
|
||||||
private class ModelFunction extends MongooseFunction {
|
Model() { this = getAMongooseInstance().getAMemberCall("model") }
|
||||||
string methodName;
|
|
||||||
|
|
||||||
ModelFunction() { this = getModelObject().getMember(methodName) }
|
|
||||||
|
|
||||||
override API::Node getQueryReturn() {
|
|
||||||
MethodSignatures::returnsQuery(methodName) and result = this.getReturn()
|
|
||||||
}
|
|
||||||
|
|
||||||
override predicate returnsDocumentQuery(boolean asArray) {
|
|
||||||
MethodSignatures::returnsDocumentQuery(methodName, asArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
override API::Node getQueryArgument() {
|
|
||||||
exists(int n |
|
|
||||||
MethodSignatures::interpretsArgumentAsQuery(methodName, n) and
|
|
||||||
result = this.getParameter(n)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a API-graph node referring to a Mongoose Model object.
|
|
||||||
*/
|
|
||||||
private API::Node getModelObject() {
|
|
||||||
result = getAMongooseInstance().getMember("model").getReturn()
|
|
||||||
or
|
|
||||||
exists(API::Node conn | conn = createConnection().getReturn() |
|
|
||||||
result = conn.getMember("model").getReturn() or
|
|
||||||
result = conn.getMember("models").getAMember()
|
|
||||||
)
|
|
||||||
or
|
|
||||||
result = API::Node::ofType("mongoose", "Model")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides signatures for the Model methods.
|
|
||||||
*/
|
|
||||||
module MethodSignatures {
|
|
||||||
/**
|
|
||||||
* Holds if Model method `name` interprets parameter `n` as a query.
|
|
||||||
*/
|
|
||||||
predicate interpretsArgumentAsQuery(string name, int n) {
|
|
||||||
// implement lots of the MongoDB collection interface
|
|
||||||
MongoDB::CollectionMethodSignatures::interpretsArgumentAsQuery(name, n)
|
|
||||||
or
|
|
||||||
name = "find" + ["ById", "One"] + "AndUpdate" and n = 1
|
|
||||||
or
|
|
||||||
name in ["delete" + ["Many", "One"], "geoSearch", "remove", "replaceOne", "where"] and
|
|
||||||
n = 0
|
|
||||||
or
|
|
||||||
name in [
|
|
||||||
"find" + ["", "ById", "One"],
|
|
||||||
"find" + ["ById", "One"] + "And" + ["Delete", "Remove", "Update"],
|
|
||||||
"update" + ["", "Many", "One"]
|
|
||||||
] and
|
|
||||||
n = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds if Model method `name` returns a Query.
|
|
||||||
*/
|
|
||||||
predicate returnsQuery(string name) {
|
|
||||||
name =
|
|
||||||
[
|
|
||||||
"$where", "count", "findOne", "findOneAndDelete", "findOneAndRemove",
|
|
||||||
"findOneAndReplace", "findOneAndUpdate", "geosearch", "remove", "replaceOne", "update",
|
|
||||||
"updateMany", "countDocuments", "updateOne", "where", "deleteMany", "deleteOne", "find",
|
|
||||||
"findById", "findByIdAndDelete", "findByIdAndRemove", "findByIdAndUpdate"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds if Document method `name` returns a query that results in
|
|
||||||
* one or more documents, the documents are wrapped in an array
|
|
||||||
* if `asArray` is true.
|
|
||||||
*/
|
|
||||||
predicate returnsDocumentQuery(string name, boolean asArray) {
|
|
||||||
asArray = false and name = "findOne"
|
|
||||||
or
|
|
||||||
asArray = true and name = "find"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides classes modeling the Mongoose Query class
|
* A collection based on the type `mongodb.Collection`.
|
||||||
*/
|
*
|
||||||
module Query {
|
* Note that this also covers `mongoose` models since they are subtypes
|
||||||
private class QueryFunction extends MongooseFunction {
|
* of `mongodb.Collection`.
|
||||||
string methodName;
|
|
||||||
|
|
||||||
QueryFunction() { this = getAMongooseQuery().getMember(methodName) }
|
|
||||||
|
|
||||||
override API::Node getQueryReturn() {
|
|
||||||
MethodSignatures::returnsQuery(methodName) and result = this.getReturn()
|
|
||||||
}
|
|
||||||
|
|
||||||
override predicate returnsDocumentQuery(boolean asArray) {
|
|
||||||
MethodSignatures::returnsDocumentQuery(methodName, asArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
override API::Node getQueryArgument() {
|
|
||||||
exists(int n |
|
|
||||||
MethodSignatures::interpretsArgumentAsQuery(methodName, n) and
|
|
||||||
result = this.getParameter(n)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class NewQueryFunction extends MongooseFunction {
|
|
||||||
NewQueryFunction() { this = getAMongooseInstance().getMember("Query") }
|
|
||||||
|
|
||||||
override API::Node getQueryReturn() { result = this.getInstance() }
|
|
||||||
|
|
||||||
override predicate returnsDocumentQuery(boolean asArray) { none() }
|
|
||||||
|
|
||||||
override API::Node getQueryArgument() { result = this.getParameter(2) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a data flow node referring to a Mongoose query object.
|
|
||||||
*/
|
|
||||||
API::Node getAMongooseQuery() {
|
|
||||||
result = any(MongooseFunction f).getQueryReturn()
|
|
||||||
or
|
|
||||||
result = API::Node::ofType("mongoose", "Query")
|
|
||||||
or
|
|
||||||
result =
|
|
||||||
getAMongooseQuery()
|
|
||||||
.getMember(any(string name | MethodSignatures::returnsQuery(name)))
|
|
||||||
.getReturn()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides signatures for the Query methods.
|
|
||||||
*/
|
|
||||||
module MethodSignatures {
|
|
||||||
/**
|
|
||||||
* Holds if Query method `name` interprets parameter `n` as a query.
|
|
||||||
*/
|
|
||||||
predicate interpretsArgumentAsQuery(string name, int n) {
|
|
||||||
n = 0 and
|
|
||||||
name =
|
|
||||||
[
|
|
||||||
"and", "count", "findOneAndReplace", "findOneAndUpdate", "merge", "nor", "or", "remove",
|
|
||||||
"replaceOne", "setQuery", "setUpdate", "update", "countDocuments", "updateMany",
|
|
||||||
"updateOne", "where", "deleteMany", "deleteOne", "elemMatch", "find", "findOne",
|
|
||||||
"findOneAndDelete", "findOneAndRemove"
|
|
||||||
]
|
|
||||||
or
|
|
||||||
n = 1 and
|
|
||||||
name = ["distinct", "findOneAndUpdate", "update", "updateMany", "updateOne"]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds if Query method `name` returns a Query.
|
|
||||||
*/
|
|
||||||
predicate returnsQuery(string name) {
|
|
||||||
name =
|
|
||||||
[
|
|
||||||
"$where", "J", "comment", "count", "countDocuments", "distinct", "elemMatch", "equals",
|
|
||||||
"error", "estimatedDocumentCount", "exists", "explain", "all", "find", "findById",
|
|
||||||
"findOne", "findOneAndRemove", "findOneAndUpdate", "geometry", "get", "gt", "gte",
|
|
||||||
"hint", "and", "in", "intersects", "lean", "limit", "lt", "lte", "map", "map",
|
|
||||||
"maxDistance", "maxTimeMS", "batchsize", "maxscan", "mod", "ne", "near", "nearSphere",
|
|
||||||
"nin", "or", "orFail", "polygon", "populate", "box", "read", "readConcern", "regexp",
|
|
||||||
"remove", "select", "session", "set", "setOptions", "setQuery", "setUpdate", "center",
|
|
||||||
"size", "skip", "slaveOk", "slice", "snapshot", "sort", "update", "w", "where",
|
|
||||||
"within", "centerSphere", "wtimeout", "circle", "collation"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds if Query method `name` returns a query that results in
|
|
||||||
* one or more documents, the documents are wrapped in an array
|
|
||||||
* if `asArray` is true.
|
|
||||||
*/
|
|
||||||
predicate returnsDocumentQuery(string name, boolean asArray) {
|
|
||||||
asArray = false and name = "findOne"
|
|
||||||
or
|
|
||||||
asArray = true and name = "find"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides classes modeling the Mongoose Document class
|
|
||||||
*/
|
|
||||||
module Document {
|
|
||||||
private class DocumentFunction extends MongooseFunction {
|
|
||||||
string methodName;
|
|
||||||
|
|
||||||
DocumentFunction() { this = getAMongooseDocument().getMember(methodName) }
|
|
||||||
|
|
||||||
override API::Node getQueryReturn() {
|
|
||||||
MethodSignatures::returnsQuery(methodName) and result = this.getReturn()
|
|
||||||
}
|
|
||||||
|
|
||||||
override predicate returnsDocumentQuery(boolean asArray) {
|
|
||||||
MethodSignatures::returnsDocumentQuery(methodName, asArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
override API::Node getQueryArgument() {
|
|
||||||
exists(int n |
|
|
||||||
MethodSignatures::interpretsArgumentAsQuery(methodName, n) and
|
|
||||||
result = this.getParameter(n)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Mongoose Document that is retrieved from the backing database.
|
|
||||||
*/
|
|
||||||
class RetrievedDocument extends API::Node {
|
|
||||||
RetrievedDocument() {
|
|
||||||
exists(boolean asArray, API::Node param |
|
|
||||||
exists(MongooseFunction func |
|
|
||||||
func.returnsDocumentQuery(asArray) and
|
|
||||||
param = func.getLastParameter().getParameter(1)
|
|
||||||
)
|
|
||||||
or
|
|
||||||
exists(API::Node f |
|
|
||||||
f = Query::getAMongooseQuery().getMember("then") and
|
|
||||||
param = f.getParameter(0).getParameter(0)
|
|
||||||
or
|
|
||||||
f = Query::getAMongooseQuery().getMember("exec") and
|
|
||||||
param = f.getParameter(0).getParameter(1)
|
|
||||||
|
|
|
||||||
exists(DataFlow::MethodCallNode pred |
|
|
||||||
// limitation: look at the previous method call
|
|
||||||
Query::MethodSignatures::returnsDocumentQuery(pred.getMethodName(), asArray) and
|
|
||||||
pred.getAMethodCall() = f.getACall()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
|
||||||
asArray = false and this = param
|
|
||||||
or
|
|
||||||
asArray = true and
|
|
||||||
// limitation: look for direct accesses
|
|
||||||
this = param.getUnknownMember()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a data flow node referring to a Mongoose Document object.
|
|
||||||
*/
|
|
||||||
private API::Node getAMongooseDocument() {
|
|
||||||
result instanceof RetrievedDocument
|
|
||||||
or
|
|
||||||
result = API::Node::ofType("mongoose", "Document")
|
|
||||||
or
|
|
||||||
result =
|
|
||||||
getAMongooseDocument()
|
|
||||||
.getMember(any(string name | MethodSignatures::returnsDocument(name)))
|
|
||||||
.getReturn()
|
|
||||||
}
|
|
||||||
|
|
||||||
private module MethodSignatures {
|
|
||||||
/**
|
|
||||||
* Holds if Document method `name` returns a Query.
|
|
||||||
*/
|
|
||||||
predicate returnsQuery(string name) {
|
|
||||||
// Documents are subtypes of Models
|
|
||||||
Model::MethodSignatures::returnsQuery(name) or
|
|
||||||
name = "replaceOne" or
|
|
||||||
name = "update" or
|
|
||||||
name = "updateOne"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds if Document method `name` interprets parameter `n` as a query.
|
|
||||||
*/
|
|
||||||
predicate interpretsArgumentAsQuery(string name, int n) {
|
|
||||||
// Documents are subtypes of Models
|
|
||||||
Model::MethodSignatures::interpretsArgumentAsQuery(name, n)
|
|
||||||
or
|
|
||||||
n = 0 and
|
|
||||||
(
|
|
||||||
name = "replaceOne" or
|
|
||||||
name = "update" or
|
|
||||||
name = "updateOne"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds if Document method `name` returns a query that results in
|
|
||||||
* one or more documents, the documents are wrapped in an array
|
|
||||||
* if `asArray` is true.
|
|
||||||
*/
|
|
||||||
predicate returnsDocumentQuery(string name, boolean asArray) {
|
|
||||||
// Documents are subtypes of Models
|
|
||||||
Model::MethodSignatures::returnsDocumentQuery(name, asArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds if Document method `name` returns a Document.
|
|
||||||
*/
|
|
||||||
predicate returnsDocument(string name) {
|
|
||||||
name = ["depopulate", "init", "populate", "overwrite"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An expression passed to `mongoose.createConnection` to supply credentials.
|
|
||||||
*/
|
*/
|
||||||
class Credentials extends CredentialsExpr {
|
class Credentials extends CredentialsExpr {
|
||||||
string kind;
|
string kind;
|
||||||
|
|
||||||
Credentials() {
|
Credentials() {
|
||||||
exists(string prop |
|
exists(string prop | this = createConnection().getOptionArgument(3, prop).asExpr() |
|
||||||
this = createConnection().getParameter(3).getMember(prop).getARhs().asExpr()
|
|
||||||
|
|
|
||||||
prop = "user" and kind = "user name"
|
prop = "user" and kind = "user name"
|
||||||
or
|
or
|
||||||
prop = "pass" and kind = "password"
|
prop = "pass" and kind = "password"
|
||||||
@@ -508,308 +166,4 @@ private module Mongoose {
|
|||||||
|
|
||||||
override string getCredentialsKind() { result = kind }
|
override string getCredentialsKind() { result = kind }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* An expression that is interpreted as a (part of a) MongoDB query.
|
|
||||||
*/
|
|
||||||
class MongoDBQueryPart extends NoSQL::Query {
|
|
||||||
MongooseFunction f;
|
|
||||||
|
|
||||||
MongoDBQueryPart() { this = f.getQueryArgument().getARhs().asExpr() }
|
|
||||||
|
|
||||||
override DataFlow::Node getACodeOperator() {
|
|
||||||
result = getADollarWhereProperty(f.getQueryArgument())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An evaluation of a MongoDB query.
|
|
||||||
*/
|
|
||||||
class ShorthandQueryEvaluation extends DatabaseAccess, DataFlow::InvokeNode {
|
|
||||||
MongooseFunction f;
|
|
||||||
|
|
||||||
ShorthandQueryEvaluation() {
|
|
||||||
this = f.getACall() and
|
|
||||||
// shorthand for execution: provide a callback
|
|
||||||
exists(f.getQueryReturn()) and
|
|
||||||
exists(this.getCallback(this.getNumArgument() - 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAQueryArgument() {
|
|
||||||
// NB: the complete information is not easily accessible for deeply chained calls
|
|
||||||
f.getQueryArgument().getARhs() = result
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAResult() {
|
|
||||||
result = this.getCallback(this.getNumArgument() - 1).getParameter(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExplicitQueryEvaluation extends DatabaseAccess, DataFlow::CallNode {
|
|
||||||
string member;
|
|
||||||
|
|
||||||
ExplicitQueryEvaluation() {
|
|
||||||
// explicit execution using a Query method call
|
|
||||||
member = ["exec", "then", "catch"] and
|
|
||||||
Query::getAMongooseQuery().getMember(member).getACall() = this
|
|
||||||
}
|
|
||||||
|
|
||||||
private int resultParamIndex() {
|
|
||||||
member = "then" and result = 0
|
|
||||||
or
|
|
||||||
member = "exec" and result = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAResult() {
|
|
||||||
result = this.getCallback(_).getParameter(this.resultParamIndex())
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAQueryArgument() {
|
|
||||||
// NB: the complete information is not easily accessible for deeply chained calls
|
|
||||||
none()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides classes modeling the Minimongo library.
|
|
||||||
*/
|
|
||||||
private module Minimongo {
|
|
||||||
/**
|
|
||||||
* Provides signatures for the Collection methods.
|
|
||||||
*/
|
|
||||||
module CollectionMethodSignatures {
|
|
||||||
/**
|
|
||||||
* Holds if Collection method `name` interprets parameter `n` as a query.
|
|
||||||
*/
|
|
||||||
predicate interpretsArgumentAsQuery(string m, int queryArgIdx) {
|
|
||||||
// implements most of the MongoDB interface
|
|
||||||
MongoDB::CollectionMethodSignatures::interpretsArgumentAsQuery(m, queryArgIdx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A call to a Minimongo query method. */
|
|
||||||
private class QueryCall extends DatabaseAccess, API::CallNode {
|
|
||||||
int queryArgIdx;
|
|
||||||
|
|
||||||
QueryCall() {
|
|
||||||
exists(string m |
|
|
||||||
this =
|
|
||||||
API::moduleImport("minimongo")
|
|
||||||
.getAMember()
|
|
||||||
.getReturn()
|
|
||||||
.getAMember()
|
|
||||||
.getMember(m)
|
|
||||||
.getACall() and
|
|
||||||
CollectionMethodSignatures::interpretsArgumentAsQuery(m, queryArgIdx)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAQueryArgument() { result = this.getArgument(queryArgIdx) }
|
|
||||||
|
|
||||||
override DataFlow::Node getAResult() {
|
|
||||||
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
|
|
||||||
}
|
|
||||||
|
|
||||||
DataFlow::Node getACodeOperator() {
|
|
||||||
result = getADollarWhereProperty(this.getParameter(queryArgIdx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An expression that is interpreted as a Minimongo query.
|
|
||||||
*/
|
|
||||||
class Query extends NoSQL::Query {
|
|
||||||
QueryCall qc;
|
|
||||||
|
|
||||||
Query() { this = qc.getAQueryArgument().asExpr() }
|
|
||||||
|
|
||||||
override DataFlow::Node getACodeOperator() { result = qc.getACodeOperator() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides classes modeling the MarsDB library.
|
|
||||||
*/
|
|
||||||
private module MarsDB {
|
|
||||||
private class MarsDBAccess extends DatabaseAccess, DataFlow::CallNode {
|
|
||||||
string method;
|
|
||||||
|
|
||||||
MarsDBAccess() {
|
|
||||||
this =
|
|
||||||
API::moduleImport("marsdb")
|
|
||||||
.getMember("Collection")
|
|
||||||
.getInstance()
|
|
||||||
.getMember(method)
|
|
||||||
.getACall()
|
|
||||||
}
|
|
||||||
|
|
||||||
string getMethod() { result = method }
|
|
||||||
|
|
||||||
override DataFlow::Node getAResult() {
|
|
||||||
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAQueryArgument() { none() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A call to a MarsDB query method. */
|
|
||||||
private class QueryCall extends MarsDBAccess, API::CallNode {
|
|
||||||
int queryArgIdx;
|
|
||||||
|
|
||||||
QueryCall() {
|
|
||||||
exists(string m |
|
|
||||||
this.getMethod() = m and
|
|
||||||
// implements parts of the Minimongo interface
|
|
||||||
Minimongo::CollectionMethodSignatures::interpretsArgumentAsQuery(m, queryArgIdx)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAResult() {
|
|
||||||
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAQueryArgument() { result = this.getArgument(queryArgIdx) }
|
|
||||||
|
|
||||||
DataFlow::Node getACodeOperator() {
|
|
||||||
result = getADollarWhereProperty(this.getParameter(queryArgIdx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An expression that is interpreted as a MarsDB query.
|
|
||||||
*/
|
|
||||||
class Query extends NoSQL::Query {
|
|
||||||
QueryCall qc;
|
|
||||||
|
|
||||||
Query() { this = qc.getAQueryArgument().asExpr() }
|
|
||||||
|
|
||||||
override DataFlow::Node getACodeOperator() { result = qc.getACodeOperator() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides classes modeling the `Node Redis` library.
|
|
||||||
*
|
|
||||||
* Redis is an in-memory key-value store and not a database,
|
|
||||||
* but `Node Redis` can be exploited similarly to a NoSQL database by giving a method an array as argument instead of a string.
|
|
||||||
* As an example the below two invocations of `client.set` are equivalent:
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* const redis = require("redis");
|
|
||||||
* const client = redis.createClient();
|
|
||||||
* client.set("key", "value");
|
|
||||||
* client.set(["key", "value"]);
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ioredis is a very similar library. However, ioredis does not support array arguments in the same way, and is therefore not vulnerable to the same kind of type confusion.
|
|
||||||
*/
|
|
||||||
private module Redis {
|
|
||||||
/**
|
|
||||||
* Gets a `Node Redis` client.
|
|
||||||
*/
|
|
||||||
private API::Node client() {
|
|
||||||
result = API::moduleImport("redis").getMember("createClient").getReturn()
|
|
||||||
or
|
|
||||||
result = API::moduleImport("redis").getMember("RedisClient").getInstance()
|
|
||||||
or
|
|
||||||
result = client().getMember("duplicate").getReturn()
|
|
||||||
or
|
|
||||||
result = client().getMember("duplicate").getLastParameter().getParameter(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a (possibly chained) reference to a batch operation object.
|
|
||||||
* These have the same API as a redis client, except the calls are chained, and the sequence is terminated with a `.exec` call.
|
|
||||||
*/
|
|
||||||
private API::Node multi() {
|
|
||||||
result = client().getMember(["multi", "batch"]).getReturn()
|
|
||||||
or
|
|
||||||
result = multi().getAMember().getReturn()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a `Node Redis` client instance. Either a client created using `createClient()`, or a batch operation object.
|
|
||||||
*/
|
|
||||||
private API::Node redis() { result = [client(), multi()] }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides signatures for the query methods from Node Redis.
|
|
||||||
*/
|
|
||||||
module QuerySignatures {
|
|
||||||
/**
|
|
||||||
* Holds if `method` interprets parameter `argIndex` as a key, and a later parameter determines a value/field.
|
|
||||||
* Thereby the method is vulnerable if parameter `argIndex` is unexpectedly an array instead of a string, as an attacker can control arguments to Redis that the attacker was not supposed to control.
|
|
||||||
*
|
|
||||||
* Only setters and similar methods are included.
|
|
||||||
* For getter-like methods it is not generally possible to gain access "outside" of where you are supposed to have access,
|
|
||||||
* it is at most possible to get a Redis call to return more results than expected (e.g. by adding more members to [`geohash`](https://redis.io/commands/geohash)).
|
|
||||||
*/
|
|
||||||
predicate argumentIsAmbiguousKey(string method, int argIndex) {
|
|
||||||
method =
|
|
||||||
[
|
|
||||||
"set", "publish", "append", "bitfield", "decrby", "getset", "hincrby", "hincrbyfloat",
|
|
||||||
"hset", "hsetnx", "incrby", "incrbyfloat", "linsert", "lpush", "lpushx", "lset", "ltrim",
|
|
||||||
"rename", "renamenx", "rpushx", "setbit", "setex", "smove", "zincrby", "zinterstore",
|
|
||||||
"hdel", "lpush", "pfadd", "rpush", "sadd", "sdiffstore", "srem"
|
|
||||||
] and
|
|
||||||
argIndex = 0
|
|
||||||
or
|
|
||||||
method = ["bitop", "hmset", "mset", "msetnx", "geoadd"] and
|
|
||||||
argIndex in [0 .. any(DataFlow::InvokeNode invk).getNumArgument() - 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An expression that is interpreted as a key in a Node Redis call.
|
|
||||||
*/
|
|
||||||
class RedisKeyArgument extends NoSQL::Query {
|
|
||||||
RedisKeyArgument() {
|
|
||||||
exists(string method, int argIndex |
|
|
||||||
QuerySignatures::argumentIsAmbiguousKey(method, argIndex) and
|
|
||||||
this = redis().getMember(method).getParameter(argIndex).getARhs().asExpr()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An access to a database through redis
|
|
||||||
*/
|
|
||||||
class RedisDatabaseAccess extends DatabaseAccess, DataFlow::CallNode {
|
|
||||||
RedisDatabaseAccess() { this = redis().getMember(_).getACall() }
|
|
||||||
|
|
||||||
override DataFlow::Node getAResult() {
|
|
||||||
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAQueryArgument() { none() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides classes modeling the `ioredis` library.
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* import Redis from 'ioredis'
|
|
||||||
* let client = new Redis(...)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
private module IoRedis {
|
|
||||||
/**
|
|
||||||
* Gets an `ioredis` client.
|
|
||||||
*/
|
|
||||||
API::Node ioredis() { result = API::moduleImport("ioredis").getInstance() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An access to a database through ioredis
|
|
||||||
*/
|
|
||||||
class IoRedisDatabaseAccess extends DatabaseAccess, DataFlow::CallNode {
|
|
||||||
IoRedisDatabaseAccess() { this = ioredis().getMember(_).getACall() }
|
|
||||||
|
|
||||||
override DataFlow::Node getAResult() {
|
|
||||||
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAQueryArgument() { none() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -493,56 +493,11 @@ module NodeJSLib {
|
|||||||
*/
|
*/
|
||||||
module FS {
|
module FS {
|
||||||
/**
|
/**
|
||||||
* A member `member` from module `fs` or its drop-in replacements `graceful-fs`, `fs-extra`, `original-fs`.
|
* A member `member` from module `fs`.
|
||||||
*/
|
*/
|
||||||
DataFlow::SourceNode moduleMember(string member) {
|
DataFlow::SourceNode moduleMember(string member) {
|
||||||
result = fsModule(DataFlow::TypeTracker::end()).getAPropertyRead(member)
|
exists(string moduleName | moduleName = ["fs"] |
|
||||||
}
|
result = DataFlow::moduleMember(moduleName, member)
|
||||||
|
|
||||||
private DataFlow::SourceNode fsModule(DataFlow::TypeTracker t) {
|
|
||||||
exists(string moduleName |
|
|
||||||
moduleName = ["mz/fs", "original-fs", "fs-extra", "graceful-fs", "fs"]
|
|
||||||
|
|
|
||||||
result = DataFlow::moduleImport(moduleName)
|
|
||||||
or
|
|
||||||
// extra support for flexible names
|
|
||||||
result.asExpr().(Require).getArgument(0).mayHaveStringValue(moduleName)
|
|
||||||
) and
|
|
||||||
t.start()
|
|
||||||
or
|
|
||||||
t.start() and
|
|
||||||
result = DataFlow::moduleMember("fs", "promises")
|
|
||||||
or
|
|
||||||
exists(DataFlow::TypeTracker t2, DataFlow::SourceNode pred | pred = fsModule(t2) |
|
|
||||||
result = pred.track(t2, t)
|
|
||||||
or
|
|
||||||
t.continue() = t2 and
|
|
||||||
exists(Promisify::PromisifyAllCall promisifyAllCall |
|
|
||||||
result = promisifyAllCall and
|
|
||||||
pred.flowsTo(promisifyAllCall.getArgument(0))
|
|
||||||
)
|
|
||||||
or
|
|
||||||
// const fs = require('fs');
|
|
||||||
// let fs_copy = methods.reduce((obj, method) => {
|
|
||||||
// obj[method] = fs[method];
|
|
||||||
// return obj;
|
|
||||||
// }, {});
|
|
||||||
t.continue() = t2 and
|
|
||||||
exists(
|
|
||||||
DataFlow::MethodCallNode call, DataFlow::ParameterNode obj, DataFlow::SourceNode method
|
|
||||||
|
|
|
||||||
call.getMethodName() = "reduce" and
|
|
||||||
result = call and
|
|
||||||
obj = call.getABoundCallbackParameter(0, 0) and
|
|
||||||
obj.flowsTo(any(DataFlow::FunctionNode f).getAReturn()) and
|
|
||||||
exists(DataFlow::PropWrite write, DataFlow::PropRead read |
|
|
||||||
write = obj.getAPropertyWrite() and
|
|
||||||
method.flowsToExpr(write.getPropertyNameExpr()) and
|
|
||||||
method.flowsToExpr(read.getPropertyNameExpr()) and
|
|
||||||
read.getBase().getALocalSource() = fsModule(t2) and
|
|
||||||
write.getRhs() = maybePromisified(read)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -553,7 +508,7 @@ module NodeJSLib {
|
|||||||
private class NodeJSFileSystemAccess extends FileSystemAccess, DataFlow::CallNode {
|
private class NodeJSFileSystemAccess extends FileSystemAccess, DataFlow::CallNode {
|
||||||
string methodName;
|
string methodName;
|
||||||
|
|
||||||
NodeJSFileSystemAccess() { this = maybePromisified(FS::moduleMember(methodName)).getACall() }
|
NodeJSFileSystemAccess() { this = FS::moduleMember(methodName).getACall() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the name of the called method.
|
* Gets the name of the called method.
|
||||||
|
|||||||
@@ -33,56 +33,38 @@ module SQL {
|
|||||||
* Provides classes modeling the (API compatible) `mysql` and `mysql2` packages.
|
* Provides classes modeling the (API compatible) `mysql` and `mysql2` packages.
|
||||||
*/
|
*/
|
||||||
private module MySql {
|
private module MySql {
|
||||||
private string moduleName() { result = ["mysql", "mysql2", "mysql2/promise"] }
|
private DataFlow::SourceNode mysql() { result = DataFlow::moduleImport(["mysql", "mysql2"]) }
|
||||||
|
|
||||||
/** Gets the package name `mysql` or `mysql2`. */
|
private DataFlow::CallNode createPool() { result = mysql().getAMemberCall("createPool") }
|
||||||
API::Node mysql() { result = API::moduleImport(moduleName()) }
|
|
||||||
|
|
||||||
/** Gets a reference to `mysql.createConnection`. */
|
/** Gets a reference to a MySQL pool. */
|
||||||
API::Node createConnection() {
|
private DataFlow::SourceNode pool(DataFlow::TypeTracker t) {
|
||||||
result = mysql().getMember(["createConnection", "createConnectionPromise"])
|
t.start() and
|
||||||
|
result = createPool()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets a reference to `mysql.createPool`. */
|
/** Gets a reference to a MySQL pool. */
|
||||||
API::Node createPool() { result = mysql().getMember(["createPool", "createPoolCluster"]) }
|
private DataFlow::SourceNode pool() { result = pool(DataFlow::TypeTracker::end()) }
|
||||||
|
|
||||||
/** Gets a node that contains a MySQL pool created using `mysql.createPool()`. */
|
/** Gets a call to `mysql.createConnection`. */
|
||||||
API::Node pool() {
|
DataFlow::CallNode createConnection() { result = mysql().getAMemberCall("createConnection") }
|
||||||
result = createPool().getReturn()
|
|
||||||
or
|
|
||||||
result = pool().getMember("on").getReturn()
|
|
||||||
or
|
|
||||||
result = API::Node::ofType(moduleName(), ["Pool", "PoolCluster"])
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets a data flow node that contains a freshly created MySQL connection instance. */
|
/** Gets a reference to a MySQL connection instance. */
|
||||||
API::Node connection() {
|
private DataFlow::SourceNode connection(DataFlow::TypeTracker t) {
|
||||||
result = createConnection().getReturn()
|
t.start() and
|
||||||
or
|
(
|
||||||
result = createConnection().getReturn().getPromised()
|
result = createConnection()
|
||||||
or
|
or
|
||||||
result = pool().getMember("getConnection").getParameter(0).getParameter(1)
|
result = pool().getAMethodCall("getConnection").getABoundCallbackParameter(0, 1)
|
||||||
or
|
|
||||||
result = pool().getMember("getConnection").getPromised()
|
|
||||||
or
|
|
||||||
exists(API::CallNode call |
|
|
||||||
call = pool().getMember("on").getACall() and
|
|
||||||
call.getArgument(0).getStringValue() = ["connection", "acquire", "release"] and
|
|
||||||
result = call.getParameter(1).getParameter(0)
|
|
||||||
)
|
)
|
||||||
or
|
|
||||||
result = API::Node::ofType(moduleName(), ["Connection", "PoolConnection"])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets a reference to a MySQL connection instance. */
|
||||||
|
DataFlow::SourceNode connection() { result = connection(DataFlow::TypeTracker::end()) }
|
||||||
|
|
||||||
/** A call to the MySql `query` method. */
|
/** A call to the MySql `query` method. */
|
||||||
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
|
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
|
||||||
QueryCall() {
|
QueryCall() { this = [pool(), connection()].getAMethodCall("query") }
|
||||||
exists(API::Node recv | recv = pool() or recv = connection() |
|
|
||||||
this = recv.getMember(["query", "execute"]).getACall()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAResult() { result = this.getCallback(_).getParameter(1) }
|
|
||||||
|
|
||||||
override DataFlow::Node getAQueryArgument() { result = this.getArgument(0) }
|
override DataFlow::Node getAQueryArgument() { result = this.getArgument(0) }
|
||||||
}
|
}
|
||||||
@@ -95,7 +77,7 @@ private module MySql {
|
|||||||
/** A call to the `escape` or `escapeId` method that performs SQL sanitization. */
|
/** A call to the `escape` or `escapeId` method that performs SQL sanitization. */
|
||||||
class EscapingSanitizer extends SQL::SqlSanitizer, MethodCallExpr {
|
class EscapingSanitizer extends SQL::SqlSanitizer, MethodCallExpr {
|
||||||
EscapingSanitizer() {
|
EscapingSanitizer() {
|
||||||
this = [mysql(), pool(), connection()].getMember(["escape", "escapeId"]).getACall().asExpr() and
|
this = [mysql(), pool(), connection()].getAMethodCall(["escape", "escapeId"]).asExpr() and
|
||||||
input = this.getArgument(0) and
|
input = this.getArgument(0) and
|
||||||
output = this
|
output = this
|
||||||
}
|
}
|
||||||
@@ -106,9 +88,8 @@ private module MySql {
|
|||||||
string kind;
|
string kind;
|
||||||
|
|
||||||
Credentials() {
|
Credentials() {
|
||||||
exists(API::Node callee, string prop |
|
exists(string prop |
|
||||||
callee in [createConnection(), createPool()] and
|
this = [createConnection(), createPool()].getOptionArgument(0, prop).asExpr() and
|
||||||
this = callee.getParameter(0).getMember(prop).getARhs().asExpr() and
|
|
||||||
(
|
(
|
||||||
prop = "user" and kind = "user name"
|
prop = "user" and kind = "user name"
|
||||||
or
|
or
|
||||||
@@ -125,61 +106,23 @@ private module MySql {
|
|||||||
* Provides classes modeling the PostgreSQL packages, such as `pg` and `pg-promise`.
|
* Provides classes modeling the PostgreSQL packages, such as `pg` and `pg-promise`.
|
||||||
*/
|
*/
|
||||||
private module Postgres {
|
private module Postgres {
|
||||||
API::Node pg() {
|
/** Gets an expression that constructs a new connection pool. */
|
||||||
result = API::moduleImport("pg")
|
DataFlow::InvokeNode newPool() {
|
||||||
or
|
|
||||||
result = pgpMain().getMember("pg")
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets a reference to the `Client` constructor in the `pg` package, for example `require('pg').Client`. */
|
|
||||||
API::Node newClient() { result = pg().getMember("Client") }
|
|
||||||
|
|
||||||
/** Gets a freshly created Postgres client instance. */
|
|
||||||
API::Node client() {
|
|
||||||
result = newClient().getInstance()
|
|
||||||
or
|
|
||||||
// pool.connect(function(err, client) { ... })
|
|
||||||
result = pool().getMember("connect").getParameter(0).getParameter(1)
|
|
||||||
or
|
|
||||||
// await pool.connect()
|
|
||||||
result = pool().getMember("connect").getReturn().getPromised()
|
|
||||||
or
|
|
||||||
result = pgpConnection().getMember("client")
|
|
||||||
or
|
|
||||||
exists(API::CallNode call |
|
|
||||||
call = pool().getMember("on").getACall() and
|
|
||||||
call.getArgument(0).getStringValue() = ["connect", "acquire"] and
|
|
||||||
result = call.getParameter(1).getParameter(0)
|
|
||||||
)
|
|
||||||
or
|
|
||||||
result = client().getMember("on").getReturn()
|
|
||||||
or
|
|
||||||
result = API::Node::ofType("pg", ["Client", "PoolClient"])
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets a constructor that when invoked constructs a new connection pool. */
|
|
||||||
API::Node newPool() {
|
|
||||||
// new require('pg').Pool()
|
// new require('pg').Pool()
|
||||||
result = pg().getMember("Pool")
|
result = DataFlow::moduleImport("pg").getAConstructorInvocation("Pool")
|
||||||
or
|
or
|
||||||
// new require('pg-pool')
|
// new require('pg-pool')
|
||||||
result = API::moduleImport("pg-pool")
|
result = DataFlow::moduleImport("pg-pool").getAnInstantiation()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets an API node that refers to a connection pool. */
|
/** Gets a creation of a Postgres client. */
|
||||||
API::Node pool() {
|
DataFlow::InvokeNode newClient() {
|
||||||
result = newPool().getInstance()
|
result = DataFlow::moduleImport("pg").getAConstructorInvocation("Client")
|
||||||
or
|
|
||||||
result = pgpDatabase().getMember("$pool")
|
|
||||||
or
|
|
||||||
result = pool().getMember("on").getReturn()
|
|
||||||
or
|
|
||||||
result = API::Node::ofType("pg", "Pool")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A call to the Postgres `query` method. */
|
/** A call to the Postgres `query` method. */
|
||||||
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
|
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
|
||||||
QueryCall() { this = [client(), pool()].getMember("query").getACall() }
|
QueryCall() { this = [newClient(), newPool()].getAMethodCall("query") }
|
||||||
|
|
||||||
override DataFlow::Node getAResult() {
|
override DataFlow::Node getAResult() {
|
||||||
this.getNumArgument() = 2 and
|
this.getNumArgument() = 2 and
|
||||||
@@ -208,11 +151,7 @@ private module Postgres {
|
|||||||
string kind;
|
string kind;
|
||||||
|
|
||||||
Credentials() {
|
Credentials() {
|
||||||
exists(string prop |
|
exists(string prop | this = [newClient(), newPool()].getOptionArgument(0, prop).asExpr() |
|
||||||
this = [newClient(), newPool()].getParameter(0).getMember(prop).getARhs().asExpr()
|
|
||||||
or
|
|
||||||
this = pgPromise().getParameter(0).getMember(prop).getARhs().asExpr()
|
|
||||||
|
|
|
||||||
prop = "user" and kind = "user name"
|
prop = "user" and kind = "user name"
|
||||||
or
|
or
|
||||||
prop = "password" and kind = prop
|
prop = "password" and kind = prop
|
||||||
@@ -357,40 +296,30 @@ private module Postgres {
|
|||||||
*/
|
*/
|
||||||
private module Sqlite {
|
private module Sqlite {
|
||||||
/** Gets a reference to the `sqlite3` module. */
|
/** Gets a reference to the `sqlite3` module. */
|
||||||
API::Node sqlite() {
|
DataFlow::SourceNode sqlite() {
|
||||||
result = API::moduleImport("sqlite3")
|
result = DataFlow::moduleImport("sqlite3")
|
||||||
or
|
or
|
||||||
result = sqlite().getMember("verbose").getReturn()
|
result = sqlite().getAMemberCall("verbose")
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets an expression that constructs or returns a Sqlite database instance. */
|
/** Gets an expression that constructs a Sqlite database instance. */
|
||||||
API::Node database() {
|
DataFlow::SourceNode newDb() {
|
||||||
// new require('sqlite3').Database()
|
// new require('sqlite3').Database()
|
||||||
result = sqlite().getMember("Database").getInstance()
|
result = sqlite().getAConstructorInvocation("Database")
|
||||||
or
|
|
||||||
// chained call
|
|
||||||
result = getAChainingQueryCall()
|
|
||||||
or
|
|
||||||
result = API::Node::ofType("sqlite3", "Database")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A call to a query method on a Sqlite database instance that returns the same instance. */
|
/** Gets a data flow node referring to a Sqlite database instance. */
|
||||||
private API::Node getAChainingQueryCall() {
|
private DataFlow::SourceNode db(DataFlow::TypeTracker t) {
|
||||||
result = database().getMember(["all", "each", "exec", "get", "run"]).getReturn()
|
t.start() and
|
||||||
|
result = newDb()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets a data flow node referring to a Sqlite database instance. */
|
||||||
|
DataFlow::SourceNode db() { result = db(DataFlow::TypeTracker::end()) }
|
||||||
|
|
||||||
/** A call to a Sqlite query method. */
|
/** A call to a Sqlite query method. */
|
||||||
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
|
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
|
||||||
QueryCall() {
|
QueryCall() { this = db().getAMethodCall(["all", "each", "exec", "get", "prepare", "run"]) }
|
||||||
this = getAChainingQueryCall().getAnImmediateUse()
|
|
||||||
or
|
|
||||||
this = database().getMember("prepare").getACall()
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAResult() {
|
|
||||||
result = this.getCallback(1).getParameter(1) or
|
|
||||||
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAQueryArgument() { result = this.getArgument(0) }
|
override DataFlow::Node getAQueryArgument() { result = this.getArgument(0) }
|
||||||
}
|
}
|
||||||
@@ -400,203 +329,3 @@ private module Sqlite {
|
|||||||
QueryString() { this = any(QueryCall qc).getAQueryArgument().asExpr() }
|
QueryString() { this = any(QueryCall qc).getAQueryArgument().asExpr() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides classes modeling the `mssql` package.
|
|
||||||
*/
|
|
||||||
private module MsSql {
|
|
||||||
/** Gets a reference to the `mssql` module. */
|
|
||||||
API::Node mssql() { result = API::moduleImport("mssql") }
|
|
||||||
|
|
||||||
/** Gets a node referring to an instance of the given class. */
|
|
||||||
API::Node mssqlClass(string name) {
|
|
||||||
result = mssql().getMember(name).getInstance()
|
|
||||||
or
|
|
||||||
result = API::Node::ofType("mssql", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets an API node referring to a Request object. */
|
|
||||||
API::Node request() {
|
|
||||||
result = mssqlClass("Request")
|
|
||||||
or
|
|
||||||
result = request().getMember(["input", "replaceInput", "output", "replaceOutput"]).getReturn()
|
|
||||||
or
|
|
||||||
result = [transaction(), pool()].getMember("request").getReturn()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets an API node referring to a Transaction object. */
|
|
||||||
API::Node transaction() {
|
|
||||||
result = mssqlClass("Transaction")
|
|
||||||
or
|
|
||||||
result = pool().getMember("transaction").getReturn()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets a API node referring to a ConnectionPool object. */
|
|
||||||
API::Node pool() { result = mssqlClass("ConnectionPool") }
|
|
||||||
|
|
||||||
/** A tagged template evaluated as a query. */
|
|
||||||
private class QueryTemplateExpr extends DatabaseAccess, DataFlow::ValueNode, DataFlow::SourceNode {
|
|
||||||
override TaggedTemplateExpr astNode;
|
|
||||||
|
|
||||||
QueryTemplateExpr() {
|
|
||||||
mssql().getMember("query").getAUse() = DataFlow::valueNode(astNode.getTag())
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAResult() {
|
|
||||||
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAQueryArgument() {
|
|
||||||
result = DataFlow::valueNode(astNode.getTemplate().getAnElement())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A call to a MsSql query method. */
|
|
||||||
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
|
|
||||||
QueryCall() { this = [mssql(), request()].getMember(["query", "batch"]).getACall() }
|
|
||||||
|
|
||||||
override DataFlow::Node getAResult() {
|
|
||||||
result = this.getCallback(1).getParameter(1)
|
|
||||||
or
|
|
||||||
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
|
|
||||||
}
|
|
||||||
|
|
||||||
override DataFlow::Node getAQueryArgument() { result = this.getArgument(0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An expression that is passed to a method that interprets it as SQL. */
|
|
||||||
class QueryString extends SQL::SqlString {
|
|
||||||
QueryString() {
|
|
||||||
exists(DatabaseAccess dba | dba instanceof QueryTemplateExpr or dba instanceof QueryCall |
|
|
||||||
this = dba.getAQueryArgument().asExpr()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An element of a query template, which is automatically sanitized. */
|
|
||||||
class QueryTemplateSanitizer extends SQL::SqlSanitizer {
|
|
||||||
QueryTemplateSanitizer() {
|
|
||||||
this = any(QueryTemplateExpr qte).getAQueryArgument().asExpr() and
|
|
||||||
input = this and
|
|
||||||
output = this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An expression that is passed as user name or password when creating a client or a pool. */
|
|
||||||
class Credentials extends CredentialsExpr {
|
|
||||||
string kind;
|
|
||||||
|
|
||||||
Credentials() {
|
|
||||||
exists(API::Node callee, string prop |
|
|
||||||
(
|
|
||||||
callee = mssql().getMember("connect")
|
|
||||||
or
|
|
||||||
callee = mssql().getMember("ConnectionPool")
|
|
||||||
) and
|
|
||||||
this = callee.getParameter(0).getMember(prop).getARhs().asExpr() and
|
|
||||||
(
|
|
||||||
prop = "user" and kind = "user name"
|
|
||||||
or
|
|
||||||
prop = "password" and kind = prop
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override string getCredentialsKind() { result = kind }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides classes modeling the `sequelize` package.
|
|
||||||
*/
|
|
||||||
private module Sequelize {
|
|
||||||
class SequelizeModel extends ModelInput::TypeModelCsv {
|
|
||||||
override predicate row(string row) {
|
|
||||||
// package1;type1;package2;type2;path
|
|
||||||
row =
|
|
||||||
[
|
|
||||||
"sequelize;;sequelize-typescript;;", //
|
|
||||||
"sequelize;Sequelize;sequelize;default;", //
|
|
||||||
"sequelize;Sequelize;sequelize;;Instance",
|
|
||||||
"sequelize;Sequelize;sequelize;;Member[Sequelize].Instance",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SequelizeSink extends ModelInput::SinkModelCsv {
|
|
||||||
override predicate row(string row) {
|
|
||||||
row =
|
|
||||||
[
|
|
||||||
"sequelize;Sequelize;Member[query].Argument[0];sql-injection",
|
|
||||||
"sequelize;Sequelize;Member[query].Argument[0].Member[query];sql-injection",
|
|
||||||
"sequelize;;Member[literal,asIs].Argument[0];sql-injection",
|
|
||||||
"sequelize;;Argument[1];credentials[user name]",
|
|
||||||
"sequelize;;Argument[2];credentials[password]",
|
|
||||||
"sequelize;;Argument[0..].Member[username];credentials[user name]",
|
|
||||||
"sequelize;;Argument[0..].Member[password];credentials[password]"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SequelizeSource extends ModelInput::SourceModelCsv {
|
|
||||||
override predicate row(string row) {
|
|
||||||
row = "sequelize;Sequelize;Member[query].ReturnValue.Awaited;database-access-result"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private module SpannerCsv {
|
|
||||||
class SpannerTypes extends ModelInput::TypeModelCsv {
|
|
||||||
override predicate row(string row) {
|
|
||||||
// package1; type1; package2; type2; path
|
|
||||||
row =
|
|
||||||
[
|
|
||||||
"@google-cloud/spanner;;@google-cloud/spanner;;Member[Spanner]",
|
|
||||||
"@google-cloud/spanner;Database;@google-cloud/spanner;;ReturnValue.Member[instance].ReturnValue.Member[database].ReturnValue",
|
|
||||||
"@google-cloud/spanner;v1.SpannerClient;@google-cloud/spanner;;Member[v1].Member[SpannerClient].Instance",
|
|
||||||
"@google-cloud/spanner;Transaction;@google-cloud/spanner;Database;Member[runTransaction,runTransactionAsync,getTransaction].Argument[0..1].Parameter[1]",
|
|
||||||
"@google-cloud/spanner;Transaction;@google-cloud/spanner;Database;Member[getTransaction].ReturnValue.Awaited",
|
|
||||||
"@google-cloud/spanner;Snapshot;@google-cloud/spanner;Database;Member[getSnapshot].Argument[0..1].Parameter[1]",
|
|
||||||
"@google-cloud/spanner;Snapshot;@google-cloud/spanner;Database;Member[getSnapshot].ReturnValue.Awaited",
|
|
||||||
"@google-cloud/spanner;BatchTransaction;@google-cloud/spanner;Database;Member[batchTransaction].ReturnValue",
|
|
||||||
"@google-cloud/spanner;BatchTransaction;@google-cloud/spanner;Database;Member[createBatchTransaction].ReturnValue.Awaited",
|
|
||||||
"@google-cloud/spanner;~SqlExecutorDirect;@google-cloud/spanner;Database;Member[run,runPartitionedUpdate,runStream]",
|
|
||||||
"@google-cloud/spanner;~SqlExecutorDirect;@google-cloud/spanner;Transaction;Member[run,runStream,runUpdate]",
|
|
||||||
"@google-cloud/spanner;~SqlExecutorDirect;@google-cloud/spanner;BatchTransaction;Member[createQueryPartitions]",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SpannerSinks extends ModelInput::SinkModelCsv {
|
|
||||||
override predicate row(string row) {
|
|
||||||
// package; type; path; kind
|
|
||||||
row =
|
|
||||||
[
|
|
||||||
"@google-cloud/spanner;~SqlExecutorDirect;Argument[0];sql-injection",
|
|
||||||
"@google-cloud/spanner;~SqlExecutorDirect;Argument[0].Member[sql];sql-injection",
|
|
||||||
"@google-cloud/spanner;Transaction;Member[batchUpdate].Argument[0];sql-injection",
|
|
||||||
"@google-cloud/spanner;Transaction;Member[batchUpdate].Argument[0].ArrayElement.Member[sql];sql-injection",
|
|
||||||
"@google-cloud/spanner;v1.SpannerClient;Member[executeSql,executeStreamingSql].Argument[0].Member[sql];sql-injection",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SpannerSources extends ModelInput::SourceModelCsv {
|
|
||||||
string spannerClass() { result = ["v1.SpannerClient", "Database", "Transaction", "Snapshot",] }
|
|
||||||
|
|
||||||
string resultPath() {
|
|
||||||
result =
|
|
||||||
[
|
|
||||||
"Member[executeSql].Argument[0..].Parameter[1]",
|
|
||||||
"Member[executeSql].ReturnValue.Awaited.Member[0]", "Member[run].ReturnValue.Awaited",
|
|
||||||
"Member[run].Argument[0..].Parameter[1]",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override predicate row(string row) {
|
|
||||||
row =
|
|
||||||
"@google-cloud/spanner;" + this.spannerClass() + ";" + this.resultPath() +
|
|
||||||
";database-access-result"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,25 +36,7 @@ module ParseTorrent {
|
|||||||
* An access to user-controlled torrent information.
|
* An access to user-controlled torrent information.
|
||||||
*/
|
*/
|
||||||
class UserControlledTorrentInfo extends RemoteFlowSource {
|
class UserControlledTorrentInfo extends RemoteFlowSource {
|
||||||
UserControlledTorrentInfo() {
|
UserControlledTorrentInfo() { none() }
|
||||||
exists(DataFlow::SourceNode ref, DataFlow::PropRead read |
|
|
||||||
ref = parsedTorrentRef() and
|
|
||||||
read = ref.getAPropertyRead() and
|
|
||||||
this = read
|
|
||||||
|
|
|
||||||
exists(string prop |
|
|
||||||
not (
|
|
||||||
prop = "private" or
|
|
||||||
prop = "infoHash" or
|
|
||||||
prop = "length"
|
|
||||||
// "pieceLength" and "lastPieceLength" are not guaranteed to be numbers as of commit ae3ad15d
|
|
||||||
) and
|
|
||||||
read.getPropertyName() = prop
|
|
||||||
)
|
|
||||||
or
|
|
||||||
not exists(read.getPropertyName())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override string getSourceType() { result = "torrent information" }
|
override string getSourceType() { result = "torrent information" }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -480,8 +480,6 @@ module JQuery {
|
|||||||
private DataFlow::SourceNode dollar(DataFlow::TypeTracker t) {
|
private DataFlow::SourceNode dollar(DataFlow::TypeTracker t) {
|
||||||
t.start() and
|
t.start() and
|
||||||
result = dollarSource()
|
result = dollarSource()
|
||||||
or
|
|
||||||
exists(DataFlow::TypeTracker t2 | result = dollar(t2).track(t2, t))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -515,14 +513,6 @@ module JQuery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A `this` node in a JQuery plugin function, which is a JQuery object.
|
|
||||||
*/
|
|
||||||
private class JQueryPluginThisObject extends Range {
|
|
||||||
JQueryPluginThisObject() {
|
|
||||||
this = DataFlow::thisNode(any(JQueryPluginMethod method).getFunction())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A source of jQuery objects from the AST-based `JQueryObject` class. */
|
/** A source of jQuery objects from the AST-based `JQueryObject` class. */
|
||||||
|
|||||||
@@ -229,14 +229,8 @@ module CodeInjection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A code operator of a NoSQL query as a code injection sink.
|
* The first argument to `Module.prototype._compile` from the Node.js built-in module `module`,
|
||||||
*/
|
* considered as a code-injection sink.
|
||||||
class NoSQLCodeInjectionSink extends Sink {
|
|
||||||
NoSQLCodeInjectionSink() { any(NoSQL::Query q).getACodeOperator() = this }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The first argument to `Module.prototype._compile`, considered as a code-injection sink.
|
|
||||||
*/
|
*/
|
||||||
class ModuleCompileSink extends Sink {
|
class ModuleCompileSink extends Sink {
|
||||||
ModuleCompileSink() {
|
ModuleCompileSink() {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ module TaintedPath {
|
|||||||
* There are currently four flow labels, representing the different combinations of
|
* There are currently four flow labels, representing the different combinations of
|
||||||
* normalization and absoluteness.
|
* normalization and absoluteness.
|
||||||
*/
|
*/
|
||||||
abstract class PosixPath extends DataFlow::FlowLabel {
|
class PosixPath extends DataFlow::FlowLabel {
|
||||||
Normalization normalization;
|
Normalization normalization;
|
||||||
Relativeness relativeness;
|
Relativeness relativeness;
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ module TaintedPath {
|
|||||||
/**
|
/**
|
||||||
* A flow label representing an array of path elements that may include "..".
|
* A flow label representing an array of path elements that may include "..".
|
||||||
*/
|
*/
|
||||||
abstract class SplitPath extends DataFlow::FlowLabel {
|
class SplitPath extends DataFlow::FlowLabel {
|
||||||
SplitPath() { this = "splitPath" }
|
SplitPath() { this = "splitPath" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,12 +218,12 @@ module TaintedPath {
|
|||||||
output = this
|
output = this
|
||||||
or
|
or
|
||||||
// non-global replace or replace of something other than /\.\./g, /[/]/g, or /[\.]/g.
|
// non-global replace or replace of something other than /\.\./g, /[/]/g, or /[\.]/g.
|
||||||
this instanceof StringReplaceCall and
|
this.getCalleeName() = "replace" and
|
||||||
input = this.getReceiver() and
|
input = getReceiver() and
|
||||||
output = this and
|
output = this and
|
||||||
not exists(RegExpLiteral literal, RegExpTerm term |
|
not exists(RegExpLiteral literal, RegExpTerm term |
|
||||||
this.(StringReplaceCall).getRegExp().asExpr() = literal and
|
getArgument(0).getALocalSource().asExpr() = literal and
|
||||||
this.(StringReplaceCall).isGlobal() and
|
literal.isGlobal() and
|
||||||
literal.getRoot() = term
|
literal.getRoot() = term
|
||||||
|
|
|
|
||||||
term.getAMatchedString() = "/" or
|
term.getAMatchedString() = "/" or
|
||||||
@@ -247,15 +247,16 @@ module TaintedPath {
|
|||||||
/**
|
/**
|
||||||
* A call that removes all instances of "../" in the prefix of the string.
|
* A call that removes all instances of "../" in the prefix of the string.
|
||||||
*/
|
*/
|
||||||
class DotDotSlashPrefixRemovingReplace extends StringReplaceCall {
|
class DotDotSlashPrefixRemovingReplace extends DataFlow::CallNode {
|
||||||
DataFlow::Node input;
|
DataFlow::Node input;
|
||||||
DataFlow::Node output;
|
DataFlow::Node output;
|
||||||
|
|
||||||
DotDotSlashPrefixRemovingReplace() {
|
DotDotSlashPrefixRemovingReplace() {
|
||||||
input = this.getReceiver() and
|
this.getCalleeName() = "replace" and
|
||||||
|
input = getReceiver() and
|
||||||
output = this and
|
output = this and
|
||||||
exists(RegExpLiteral literal, RegExpTerm term |
|
exists(RegExpLiteral literal, RegExpTerm term |
|
||||||
this.getRegExp().asExpr() = literal and
|
getArgument(0).getALocalSource().asExpr() = literal and
|
||||||
(term instanceof RegExpStar or term instanceof RegExpPlus) and
|
(term instanceof RegExpStar or term instanceof RegExpPlus) and
|
||||||
term.getChild(0) = getADotDotSlashMatcher()
|
term.getChild(0) = getADotDotSlashMatcher()
|
||||||
|
|
|
|
||||||
@@ -297,16 +298,17 @@ module TaintedPath {
|
|||||||
/**
|
/**
|
||||||
* A call that removes all "." or ".." from a path, without also removing all forward slashes.
|
* A call that removes all "." or ".." from a path, without also removing all forward slashes.
|
||||||
*/
|
*/
|
||||||
class DotRemovingReplaceCall extends StringReplaceCall {
|
class DotRemovingReplaceCall extends DataFlow::CallNode {
|
||||||
DataFlow::Node input;
|
DataFlow::Node input;
|
||||||
DataFlow::Node output;
|
DataFlow::Node output;
|
||||||
|
|
||||||
DotRemovingReplaceCall() {
|
DotRemovingReplaceCall() {
|
||||||
input = this.getReceiver() and
|
this.getCalleeName() = "replace" and
|
||||||
|
input = getReceiver() and
|
||||||
output = this and
|
output = this and
|
||||||
this.isGlobal() and
|
|
||||||
exists(RegExpLiteral literal, RegExpTerm term |
|
exists(RegExpLiteral literal, RegExpTerm term |
|
||||||
this.getRegExp().asExpr() = literal and
|
getArgument(0).getALocalSource().asExpr() = literal and
|
||||||
|
literal.isGlobal() and
|
||||||
literal.getRoot() = term and
|
literal.getRoot() = term and
|
||||||
not term.getAMatchedString() = "/"
|
not term.getAMatchedString() = "/"
|
||||||
|
|
|
|
||||||
@@ -606,8 +608,6 @@ module TaintedPath {
|
|||||||
(
|
(
|
||||||
this = fileSystemAccess.getAPathArgument() and
|
this = fileSystemAccess.getAPathArgument() and
|
||||||
not exists(fileSystemAccess.getRootPathArgument())
|
not exists(fileSystemAccess.getRootPathArgument())
|
||||||
or
|
|
||||||
this = fileSystemAccess.getRootPathArgument()
|
|
||||||
) and
|
) and
|
||||||
not this = any(ResolvingPathCall call).getInput()
|
not this = any(ResolvingPathCall call).getInput()
|
||||||
}
|
}
|
||||||
@@ -648,74 +648,6 @@ module TaintedPath {
|
|||||||
AngularJSTemplateUrlSink() { this = any(AngularJS::CustomDirective d).getMember("templateUrl") }
|
AngularJSTemplateUrlSink() { this = any(AngularJS::CustomDirective d).getMember("templateUrl") }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The path argument of a [send](https://www.npmjs.com/package/send) call, viewed as a sink.
|
|
||||||
*/
|
|
||||||
class SendPathSink extends Sink, DataFlow::ValueNode {
|
|
||||||
SendPathSink() { this = DataFlow::moduleImport("send").getACall().getArgument(1) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A path argument given to a `Page` in puppeteer, specifying where a pdf/screenshot should be saved.
|
|
||||||
*/
|
|
||||||
private class PuppeteerPath extends TaintedPath::Sink {
|
|
||||||
PuppeteerPath() {
|
|
||||||
this =
|
|
||||||
Puppeteer::page()
|
|
||||||
.getMember(["pdf", "screenshot"])
|
|
||||||
.getParameter(0)
|
|
||||||
.getMember("path")
|
|
||||||
.getARhs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An argument given to the `prettier` library specifying the location of a config file.
|
|
||||||
*/
|
|
||||||
private class PrettierFileSink extends TaintedPath::Sink {
|
|
||||||
PrettierFileSink() {
|
|
||||||
this =
|
|
||||||
API::moduleImport("prettier")
|
|
||||||
.getMember(["resolveConfig", "resolveConfigFile", "getFileInfo"])
|
|
||||||
.getACall()
|
|
||||||
.getArgument(0)
|
|
||||||
or
|
|
||||||
this =
|
|
||||||
API::moduleImport("prettier")
|
|
||||||
.getMember("resolveConfig")
|
|
||||||
.getACall()
|
|
||||||
.getParameter(1)
|
|
||||||
.getMember("config")
|
|
||||||
.getARhs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `cwd` option for the `read-pkg` library.
|
|
||||||
*/
|
|
||||||
private class ReadPkgCwdSink extends TaintedPath::Sink {
|
|
||||||
ReadPkgCwdSink() {
|
|
||||||
this =
|
|
||||||
API::moduleImport("read-pkg")
|
|
||||||
.getMember(["readPackageAsync", "readPackageSync"])
|
|
||||||
.getParameter(0)
|
|
||||||
.getMember("cwd")
|
|
||||||
.getARhs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `cwd` option to a shell execution.
|
|
||||||
*/
|
|
||||||
private class ShellCwdSink extends TaintedPath::Sink {
|
|
||||||
ShellCwdSink() {
|
|
||||||
exists(SystemCommandExecution sys, API::Node opts |
|
|
||||||
opts.getARhs() = sys.getOptionsArg() and // assuming that an API::Node exists here.
|
|
||||||
this = opts.getMember("cwd").getARhs()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds if there is a step `src -> dst` mapping `srclabel` to `dstlabel` relevant for path traversal vulnerabilities.
|
* Holds if there is a step `src -> dst` mapping `srclabel` to `dstlabel` relevant for path traversal vulnerabilities.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -183,17 +183,6 @@ module DomBasedXss {
|
|||||||
this = any(Typeahead::TypeaheadSuggestionFunction f).getAReturn()
|
this = any(Typeahead::TypeaheadSuggestionFunction f).getAReturn()
|
||||||
or
|
or
|
||||||
this = any(Handlebars::SafeString s).getAnArgument()
|
this = any(Handlebars::SafeString s).getAnArgument()
|
||||||
or
|
|
||||||
this = any(JQuery::MethodCall call | call.getMethodName() = "jGrowl").getArgument(0)
|
|
||||||
or
|
|
||||||
// A construction of a JSDOM object (server side DOM), where scripts are allowed.
|
|
||||||
exists(DataFlow::NewNode instance |
|
|
||||||
instance = API::moduleImport("jsdom").getMember("JSDOM").getInstance().getAnImmediateUse() and
|
|
||||||
this = instance.getArgument(0) and
|
|
||||||
instance.getOptionArgument(1, "runScripts").mayHaveStringValue("dangerously")
|
|
||||||
)
|
|
||||||
or
|
|
||||||
MooTools::interpretsNodeAsHtml(this)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user