Compare commits

...

24 Commits

Author SHA1 Message Date
Max Schaefer
5726591780 Revert "JS: Recognize DomSanitizer from @angular/core"
This reverts commit ff1d0cc4c7.
2021-11-22 21:08:49 +00:00
Henry Mercer
cc3ad50c39 Remove NoSQL sinks since September 2018 2021-11-22 21:08:49 +00:00
Esben Sparre Andreasen
11cd44ca69 Remove additional Xss sinks 2021-11-22 21:08:49 +00:00
Esben Sparre Andreasen
78c4690eee Remove additional SQL sinks 2021-11-22 21:08:49 +00:00
Esben Sparre Andreasen
fddc2cc6de Remove additional path-injection sinks 2021-11-22 21:08:49 +00:00
Esben Sparre Andreasen
700c1f5a35 Add benjamin-button.md 2021-11-22 21:08:49 +00:00
Esben Sparre Andreasen
426e635437 Remove pseudo-properties 2021-11-22 21:08:49 +00:00
Esben Sparre Andreasen
a769c96aa2 Remove 2020 sinks from SqlInjection.ql 2021-11-22 21:08:49 +00:00
Esben Sparre Andreasen
5b1bfe2799 Remove 2020 sinks from Xss.ql 2021-11-22 21:08:49 +00:00
Esben Sparre Andreasen
6e2897b0f8 Remove 2020 sinks from TaintedPath.ql 2021-11-22 21:08:49 +00:00
Tiferet Gazit
f28b85c059 Create many features with different scopes.
This will allow us to experiment with feature selection on the modeling side to pick out the best subset of scoped features.
2021-11-22 20:56:41 +00:00
Tiferet Gazit
b493702a5a Bug fix:
When getting the wrapper for the root node, we must give the container of the root node, not the endpoint, because they won't necessarily be the same for functions contained within functions.
2021-11-22 20:15:48 +00:00
Tiferet Gazit
e2ffba2e47 TODO Interim fix:
For now don't allow the neighborhood to go all the way out to the outermostEnclosingFunction, because that causes bugs.

After this change, `enclosingFunctionBodyEndpointNeighborhood` doesn't match `enclosingFunctionBody` even when the function is small, which is undesired behavior. `enclosingFunctionBody` includes the arguments to the function whereas `enclosingFunctionBodyEndpointNeighborhood` does not. We should find a way to fix this.
2021-11-22 20:10:24 +00:00
Tiferet Gazit
a507d6dc1d Formatting fixes 2021-11-22 20:02:28 +00:00
Tiferet Gazit
da5f664265 Create three features with different scopes:
Create three versions of `enclosingFunctionBodyEndpointNeighborhood`, each with a different threshold on the number of descendants.
2021-11-20 02:39:01 +00:00
Tiferet Gazit
d52af5599a Allow the neighborhood to be the entire enclosing function 2021-11-20 02:28:53 +00:00
Anna Railton
05460f6444 Add check for neighborhood node being top-level enclosing function 2021-11-18 16:34:06 +00:00
Anna Railton
c7f96928dc Reformat docstrings 2021-11-18 16:34:03 +00:00
Anna Railton
4351d9e861 Add predicates for the magic numbers in the predicates 2021-11-18 16:26:59 +00:00
Anna Railton
c3766477dc Apply suggestions from code review
Co-authored-by: Tiferet Gazit <tiferet@github.com>
2021-11-18 16:26:59 +00:00
annarailton
b6922813a8 Add neighborhoodBody feature to token features
Co-authored-by: Chris Smowton <smowton@github.com>
2021-11-18 16:26:59 +00:00
annarailton
5de0edc773 Add NeighborhoodBodies module
This provides functionality for getting the token features associated with a
neighborhood around an AST node. It is strongly related to `FunctionBodies`.

Co-authored-by: Chris Smowton <smowton@github.com>
2021-11-18 16:26:59 +00:00
annarailton
a8c1febf88 Add helper types for changes to EndpointFeatures
Co-authored-by: Chris Smowton <smowton@github.com>
2021-11-18 16:26:59 +00:00
annarailton
2b334055db Clean up to astNodes predicate to make more explicit
Co-authored-by: Chris Smowton <smowton@github.com>
2021-11-18 16:26:59 +00:00
17 changed files with 397 additions and 1644 deletions

51
benjamin-button.md Normal file
View 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 `"\$.*\$"`.

View File

@@ -4,6 +4,8 @@
* Extracts data about the functions in the database for use in adaptive threat modeling (ATM).
*/
private import EndpointFeatures::NeighborhoodBodies
module Raw {
private import javascript as raw
@@ -30,6 +32,9 @@ module Raw {
entity.getNumBodyStmt() = 0 and not exists(entity.getAReturnedExpr())
}
/**
* Wrapper for RawAstNode (could be an alias instead of a newtype)
*/
newtype WrappedAstNode = TAstNode(RawAstNode rawNode)
/**
@@ -44,6 +49,10 @@ module Raw {
AstNode getParentNode() { result = TAstNode(rawNode.getParent()) }
raw::ASTNode getNode() { result = rawNode }
raw::StmtContainer getContainer() { result = rawNode.getContainer() }
/**
* Holds if the AST node has `result` as its `index`th attribute.
*
@@ -128,6 +137,11 @@ module Raw {
}
}
/**
* Returns the `Raw::AstNode` wrapper of `rawNode`
*/
AstNode astNode(RawAstNode rawNode) { result = TAstNode(rawNode) }
/**
* Holds if `result` is the `index`'th child of the `parent` entity. Such
* a node is a root of an AST associated with this entity.
@@ -324,6 +338,11 @@ module Wrapped {
Raw::Location getLocation() { result = rawNode.getLocation() }
}
/**
* Returns the `Wrapped::AstNode` for a `rawNode` in the context of `entity`
*/
AstNode astNode(Raw::Entity entity, Raw::AstNode rawNode) { result = TAstNode(entity, rawNode) }
/**
* A synthetic AST node, created to be a leaf for an otherwise non-leaf attribute.
*/
@@ -383,6 +402,8 @@ module DatabaseFeatures {
override Location getLocation() { result = entity.getLocation() }
UnderlyingFunction getDefinedFunction() { result = entity.getDefinedFunction() }
Wrapped::Entity getWrappedEntity() { result = entity }
}
class AstNode extends EntityOrAstNode, TAstNode {
@@ -392,8 +413,12 @@ module DatabaseFeatures {
AstNode getChild(int index) { result = TAstNode(rawNode.getChild(index)) }
AstNode getAChild() { result = this.getChild(_) }
string getAttribute(int index) { result = rawNode.getAttribute(index) }
Wrapped::AstNode getRawNode() { result = rawNode }
override string getType() { result = rawNode.getType() }
override string toString() { result = this.getType() }
@@ -401,6 +426,9 @@ module DatabaseFeatures {
override Location getLocation() { result = rawNode.getLocation() }
}
/** Gets the `DatabaseFeatures::AstNode` that wraps `wrapped` */
AstNode astNode(Wrapped::AstNode wrapped) { result.getRawNode() = wrapped }
/** Consistency checks: these predicates should each have no results */
module Consistency {
query predicate nonLeafAttribute(AstNode node, int index, string attribute) {
@@ -423,15 +451,15 @@ module DatabaseFeatures {
}
query predicate astNodes(
Entity enclosingEntity, EntityOrAstNode parent, int index, AstNode node, string node_type
Entity enclosingEntity, EntityOrAstNode parent, int index, AstNode child, string childType
) {
node = enclosingEntity.getAstRoot(index) and
child = enclosingEntity.getAstRoot(index) and
parent = enclosingEntity and
node_type = node.getType()
childType = child.getType()
or
astNodes(enclosingEntity, _, _, parent, _) and
node = parent.(AstNode).getChild(index) and
node_type = node.getType()
child = parent.(AstNode).getChild(index) and
childType = child.getType()
}
query predicate nodeAttributes(AstNode node, string attr) {

View File

@@ -8,6 +8,28 @@ import javascript
import CodeToFeatures
import EndpointScoring
/** Maximum number of descendants of an AST node to be considered to be in the "neighborhood" of that node */
class DescendantsThreshold extends int {
DescendantsThreshold() { this = [1 .. 10000] }
predicate is1024() { this = 1024 }
predicate is512() { this = 512 }
predicate is256() { this = 256 }
predicate is128() { this = 128 }
predicate is64() { this = 64 }
predicate is32() { this = 32 }
int getMaxNumDescendants() { result = this }
}
/** Maximum number of subtokens in a function body */
private int maxNumBodySubtokens() { result = 256 }
/**
* Gets the value of the token-based feature named `featureName` for the endpoint `endpoint`.
*
@@ -25,6 +47,40 @@ private string getTokenFeature(DataFlow::Node endpoint, string featureName) {
result = unique(string x | x = FunctionBodies::getBodyTokenFeatureForEntity(entity))
)
or
// A feature containing natural language tokens from the neighborhood around the endpoint
// (limited to within the function that encloses the endpoint), in the order they appear
// in the source code.
exists(
Raw::AstNode rootNode, DatabaseFeatures::AstNode rootNodeWrapped, DescendantsThreshold thresh
|
(
featureName = "enclosingFunctionBodyEndpointNeighborhood1024" and
thresh.is1024()
or
featureName = "enclosingFunctionBodyEndpointNeighborhood512" and
thresh.is512()
or
featureName = "enclosingFunctionBodyEndpointNeighborhood256" and
thresh.is256()
or
featureName = "enclosingFunctionBodyEndpointNeighborhood128" and
thresh.is128()
or
featureName = "enclosingFunctionBodyEndpointNeighborhood64" and
thresh.is64()
or
featureName = "enclosingFunctionBodyEndpointNeighborhood32" and
thresh.is32()
) and
rootNode =
NeighborhoodBodies::getNeighborhoodAstNode(Raw::astNode(endpoint.getAstNode()), thresh) and
rootNodeWrapped = DatabaseFeatures::astNode(Wrapped::astNode(rootNode.getContainer(), rootNode)) and
result =
unique(string x |
x = NeighborhoodBodies::getBodyTokenFeatureForNeighborhoodNode(rootNodeWrapped)
)
)
or
exists(getACallBasedTokenFeatureComponent(endpoint, _, featureName)) and
result =
concat(DataFlow::CallNode call, string component |
@@ -121,13 +177,17 @@ module FunctionBodies {
/**
* Gets the body token feature for the specified entity.
*
* This is a string containing natural language tokens in the order that they appear in the source code for the entity.
* This is a string containing natural language tokens in the order that they appear in the source
* code for the entity.
*
* If a function has more than `maxNumBodySubtokens` body subtokens, then featurize it as absent.
* This approximates the behavior of the classifer on non-generic body features where large body
* features are replaced by the absent token.
*/
string getBodyTokenFeatureForEntity(DatabaseFeatures::Entity entity) {
// If a function has more than 256 body subtokens, then featurize it as absent. This
// approximates the behavior of the classifer on non-generic body features where large body
// features are replaced by the absent token.
if count(DatabaseFeatures::AstNode node, string token | bodyTokens(entity, node, token)) > 256
if
count(DatabaseFeatures::AstNode node, string token | bodyTokens(entity, node, token)) >
maxNumBodySubtokens()
then result = ""
else
result =
@@ -147,6 +207,86 @@ module FunctionBodies {
}
}
/**
* This module provides functionality for getting the local neighborhood around an AST node within
* its enclosing function body, providing a locally-scoped version of the `enclosingFunctionBody` feature.
*/
module NeighborhoodBodies {
/**
* Return the ancestor of the input AST node that has the largest number of descendants (i.e. the
* node nearest the root) but has no more than `maxNumDescendants` descendants.
*
* TODO: Maybe instead of a threshold on number of descendants, we should instead have a threshold
* on the number of leaves in the subtree, which is a closer approximation to the number of tokens
* in the subtree.
*/
Raw::AstNode getNeighborhoodAstNode(Raw::AstNode node, DescendantsThreshold maxNumDescendants) {
if
node.getParentNode() = getOutermostEnclosingFunction(node) or
getNumDescendents(node.getParentNode()) > maxNumDescendants.getMaxNumDescendants()
then result = node
else result = getNeighborhoodAstNode(node.getParentNode(), maxNumDescendants)
}
/** Count number of descendants of an AST node */
int getNumDescendents(Raw::AstNode node) { result = count(node.getAChildNode*()) }
private ASTNode getContainer(ASTNode node) { result = node.getContainer() }
/** Return the AST node that is outermost enclosing function (as an AST Node) */
Raw::AstNode getOutermostEnclosingFunction(Raw::AstNode node) {
result = Raw::astNode(getContainer*(node.getNode())) and
result.getContainer() instanceof TopLevel
}
/**
* Holds if `childNode` is an AST node under `rootNode` and `token` is a node attribute associated
* with `childNode`. Note that only AST leaves have node attributes.
*
* TODO we may need to restrict `rootNode` to be a neighborhood root to avoid a potentially big result set.
*/
private predicate bodyTokens(
DatabaseFeatures::AstNode rootNode, DatabaseFeatures::AstNode childNode, string token
) {
childNode = rootNode.getAChild*() and
token = unique(string t | DatabaseFeatures::nodeAttributes(childNode, t))
}
/**
* Gets the body token feature limited to the part of the function body that lies under `rootNode` in the AST.
*
* This is a string of space-separated natural language tokens (AST leaves) in the order that they
* appear in the source code for the AST subtree rooted at `rootNode`. This is equivalent to the
* portion of the code that falls under the AST subtree rooted at the given node, except that
* non-leaf nodes (such as operators) are excluded.
*
* If a function has more than `maxNumBodySubtokens` body subtokens, then featurize it as absent.
* This approximates the behavior of the classifer on non-generic body features where large body
* features are replaced by the absent token.
*/
string getBodyTokenFeatureForNeighborhoodNode(DatabaseFeatures::AstNode rootNode) {
if
count(DatabaseFeatures::AstNode node, string token | bodyTokens(rootNode, node, token)) >
maxNumBodySubtokens()
then result = ""
else
result =
concat(int i, string rankedToken |
rankedToken =
rank[i](DatabaseFeatures::AstNode node, string token, Location l |
bodyTokens(rootNode, node, token) and l = node.getLocation()
|
token
order by
l.getFile().getAbsolutePath(), l.getStartLine(), l.getStartColumn(), l.getEndLine(),
l.getEndColumn(), token
)
|
rankedToken, " " order by i
)
}
}
/**
* This module provides functionality for getting a representation of the access path of nodes
* within the program.
@@ -191,7 +331,10 @@ private module AccessPaths {
Boolean() { this = true or this = false }
}
/** Get the access path for the node. This includes structural information like `member`, `param`, and `functionalarg` if `includeStructuralInfo` is true. */
/**
* Get the access path for the node. This includes structural information like `member`, `param`,
* and `functionalarg` if `includeStructuralInfo` is true.
*/
predicate accessPaths(
API::Node node, Boolean includeStructuralInfo, string accessPath, string apiName
) {
@@ -269,7 +412,12 @@ private string getASupportedFeatureName() {
result =
[
"enclosingFunctionName", "calleeName", "receiverName", "argumentIndex", "calleeApiName",
"calleeAccessPath", "calleeAccessPathWithStructuralInfo", "enclosingFunctionBody"
"calleeAccessPath", "calleeAccessPathWithStructuralInfo", "enclosingFunctionBody",
"enclosingFunctionBodyEndpointNeighborhood1024",
"enclosingFunctionBodyEndpointNeighborhood512",
"enclosingFunctionBodyEndpointNeighborhood256",
"enclosingFunctionBodyEndpointNeighborhood128", "enclosingFunctionBodyEndpointNeighborhood64",
"enclosingFunctionBodyEndpointNeighborhood32"
]
}

View File

@@ -359,35 +359,6 @@ module DOM {
call.getNumArgument() = 1 and
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))
}
}
}
@@ -421,11 +392,6 @@ module DOM {
or
t.start() and
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. */

View File

@@ -185,12 +185,12 @@ module Promises {
/**
* 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.
*/
string errorProp() { result = "$PromiseRejectField$" }
string errorProp() { none() }
}
/**

View File

@@ -777,10 +777,10 @@ private class AdditionalFlowStepAsSharedStep extends SharedFlowStep {
*/
module PseudoProperties {
bindingset[s]
private string pseudoProperty(string s) { result = "$" + s + "$" }
private string pseudoProperty(string s) { none() }
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`

View File

@@ -136,7 +136,7 @@ module Angular2 {
/** Gets a reference to a `DomSanitizer` object. */
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. */

View File

@@ -864,28 +864,6 @@ module Express {
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" |
calls(any(ResponseExpr res).flow(), name)
)
}
override DataFlow::Node getADataNode() { none() }
override DataFlow::Node getAPathArgument() { result = getArgument(0) }
override DataFlow::Node getRootPathArgument() {
result = this.(DataFlow::CallNode).getOptionArgument(1, "root")
}
override predicate isUpwardNavigationRejected(DataFlow::Node argument) {
argument = getAPathArgument()
}
}
/**
* A function that flows to a route setup.
*/

View File

@@ -4,23 +4,6 @@
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.
*/
@@ -143,338 +126,3 @@ private DataFlow::Node fastGlobFileNameSource(DataFlow::TypeTracker t) {
private class FastGlobFileNameSource extends FileNameSource {
FastGlobFileNameSource() { this = fastGlobFileNameSource(DataFlow::TypeTracker::end()) }
}
/**
* Classes and predicates for modelling 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" or subMod = "Dir" or subMod = "Link" or subMod = "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 = getOptionArgument(0, "path")
or
not exists(getOptionArgument(0, "path")) and
result = 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 = getArgument(0) }
override DataFlow::Node getADataNode() { result = 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 = getArgument(0) }
override DataFlow::Node getAFileName() { result = trackFileSource(DataFlow::TypeTracker::end()) }
private DataFlow::SourceNode trackFileSource(DataFlow::TypeTracker t) {
t.start() and result = getCallback([1 .. 2]).getParameter(1)
or
t.startInPromise() and not exists(getCallback([1 .. 2])) and result = this
or
// Tracking out of a promise
exists(DataFlow::TypeTracker t2 |
result = PromiseTypeTracking::promiseStep(trackFileSource(t2), t, t2)
)
}
}
/**
* Classes and predicates for modelling 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 = getArgument(0) }
override DataFlow::Node getADataNode() { result = trackRead(DataFlow::TypeTracker::end()) }
private DataFlow::SourceNode trackRead(DataFlow::TypeTracker t) {
this.getCalleeName() = "readFile" and
(
t.start() and result = getCallback([1 .. 2]).getParameter(1)
or
t.startInPromise() and not exists(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(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 = getArgument(0) }
override DataFlow::Node getADataNode() { result = 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 = getArgument(0) }
override DataFlow::Node getADataNode() { result = 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(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 = getArgument(0) }
override DataFlow::Node getADataNode() { result = 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 = getArgument(0) }
override DataFlow::Node getAFileName() { result = 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 = getCallback(getNumArgument() - 1).getParameter(0)
or
result = 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(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 = getArgument(1)
or
this.getCalleeName() = "mapping" and
(
result = getAnArgument() and not exists(result.getALocalSource().getAPropertyWrite("src"))
or
result = 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",
any(string s |
s = ["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 = 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 = getArgument(0) }
override DataFlow::Node getAFileName() {
exists(DataFlow::CallNode onCall, int pathIndex |
onCall = 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 = getArgument(0) }
}

View File

@@ -6,146 +6,120 @@ import javascript
module NoSQL {
/** An expression that is interpreted as a NoSQL query. */
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()
abstract class Query extends Expr { }
}
/**
* Provides classes modeling the MongoDB library.
*/
private module MongoDB {
/**
* Gets an import of MongoDB.
*/
DataFlow::ModuleImportNode mongodb() { result.getPath() = "mongodb" }
/**
* Gets an access to `mongodb.MongoClient`.
*/
private API::Node getAMongoClient() {
result = API::moduleImport("mongodb").getMember("MongoClient")
private DataFlow::SourceNode getAMongoClient(DataFlow::TypeTracker t) {
t.start() and
result = mongodb().getAPropertyRead("MongoClient")
or
result = getAMongoDbCallback().getParameter(1) and
not result.getAnImmediateUse().(DataFlow::ParameterNode).getName() = "db" // mongodb v2 provides a `Db` here
}
/** Gets an API-graph node that refers to a `connect` callback. */
private API::Node getAMongoDbCallback() {
result = getAMongoClient().getMember("connect").getLastParameter()
exists(DataFlow::TypeTracker t2 | result = getAMongoClient(t2).track(t2, t))
}
/**
* Gets an API-graph node that may refer to a MongoDB database connection.
* Gets an access to `mongodb.MongoClient`.
*/
private API::Node getAMongoDb() {
result = getAMongoClient().getMember("db").getReturn()
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
result = getAMongoDbCallback().getParameter(1) and
not result.getAnImmediateUse().(DataFlow::ParameterNode).getName() = "client" // mongodb v3 provides a `Mongoclient` here
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. */
private API::Node getACollection() {
// A collection resulting from calling `Db.collection(...)`.
exists(API::Node collection | collection = getAMongoDb().getMember("collection").getReturn() |
result = collection
or
result = collection.getParameter(1).getParameter(0)
)
private DataFlow::SourceNode getACollection(DataFlow::TypeTracker t) {
t.start() and
result instanceof Collection
or
// note that this also covers `mongoose` models since they are subtypes of `mongodb.Collection`
result = API::Node::ofType("mongodb", "Collection")
exists(DataFlow::TypeTracker t2 | result = getACollection(t2).track(t2, t))
}
/** 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. */
private class QueryCall extends DatabaseAccess, API::CallNode {
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
int queryArgIdx;
QueryCall() {
exists(string method |
CollectionMethodSignatures::interpretsArgumentAsQuery(method, queryArgIdx) and
this = getACollection().getMember(method).getACall()
exists(string m | this = getACollection().getAMethodCall(m) |
m = "count" and queryArgIdx = 0
or
m = "distinct" and queryArgIdx = 1
or
m = "find" and queryArgIdx = 0
)
}
override DataFlow::Node getAQueryArgument() { result = getArgument(queryArgIdx) }
DataFlow::Node getACodeOperator() {
result = getADollarWhereProperty(getParameter(queryArgIdx))
}
}
/**
* An expression that is interpreted as a MongoDB query.
*/
class Query extends NoSQL::Query {
QueryCall qc;
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
)
}
Query() { this = any(QueryCall qc).getAQueryArgument().asExpr() }
}
}
@@ -156,345 +130,20 @@ private module 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") }
/**
* 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();
DataFlow::CallNode createConnection() {
result = getAMongooseInstance().getAMemberCall("createConnection")
}
/**
* Provides classes modeling the Mongoose Model class
* A Mongoose collection object.
*/
module Model {
private class ModelFunction extends MongooseFunction {
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
*/
module Query {
private class QueryFunction extends MongooseFunction {
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" or
name = "init" or
name = "populate" or
name = "overwrite"
}
}
class Model extends MongoDB::Collection {
Model() { this = getAMongooseInstance().getAMemberCall("model") }
}
/**
@@ -504,9 +153,7 @@ private module Mongoose {
string kind;
Credentials() {
exists(string prop |
this = createConnection().getParameter(3).getMember(prop).getARhs().asExpr()
|
exists(string prop | this = createConnection().getOptionArgument(3, prop).asExpr() |
prop = "user" and kind = "user name"
or
prop = "pass" and kind = "password"
@@ -515,222 +162,4 @@ private module Mongoose {
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
}
}
class ExplicitQueryEvaluation extends DatabaseAccess {
ExplicitQueryEvaluation() {
// explicit execution using a Query method call
Query::getAMongooseQuery().getMember(["exec", "then", "catch"]).getACall() = this
}
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 = getArgument(queryArgIdx) }
DataFlow::Node getACodeOperator() {
result = getADollarWhereProperty(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 {
/** A call to a MarsDB query method. */
private class QueryCall extends DatabaseAccess, API::CallNode {
int queryArgIdx;
QueryCall() {
exists(string m |
this =
API::moduleImport("marsdb").getMember("Collection").getInstance().getMember(m).getACall() and
// implements parts of the Minimongo interface
Minimongo::CollectionMethodSignatures::interpretsArgumentAsQuery(m, queryArgIdx)
)
}
override DataFlow::Node getAQueryArgument() { result = getArgument(queryArgIdx) }
DataFlow::Node getACodeOperator() {
result = getADollarWhereProperty(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()
)
}
}
}

View File

@@ -448,56 +448,11 @@ module NodeJSLib {
*/
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) {
result = fsModule(DataFlow::TypeTracker::end()).getAPropertyRead(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)
)
)
exists(string moduleName | moduleName = ["fs"] |
result = DataFlow::moduleMember(moduleName, member)
)
}
}
@@ -508,7 +463,7 @@ module NodeJSLib {
private class NodeJSFileSystemAccess extends FileSystemAccess, DataFlow::CallNode {
string methodName;
NodeJSFileSystemAccess() { this = maybePromisified(FS::moduleMember(methodName)).getACall() }
NodeJSFileSystemAccess() { this = FS::moduleMember(methodName).getACall() }
/**
* Gets the name of the called method.

View File

@@ -28,54 +28,38 @@ module SQL {
* Provides classes modelling the (API compatible) `mysql` and `mysql2` packages.
*/
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`. */
API::Node mysql() { result = API::moduleImport(moduleName()) }
private DataFlow::CallNode createPool() { result = mysql().getAMemberCall("createPool") }
/** Gets a reference to `mysql.createConnection`. */
API::Node createConnection() {
result = mysql().getMember(["createConnection", "createConnectionPromise"])
/** Gets a reference to a MySQL pool. */
private DataFlow::SourceNode pool(DataFlow::TypeTracker t) {
t.start() and
result = createPool()
}
/** Gets a reference to `mysql.createPool`. */
API::Node createPool() { result = mysql().getMember(["createPool", "createPoolCluster"]) }
/** Gets a reference to a MySQL pool. */
private DataFlow::SourceNode pool() { result = pool(DataFlow::TypeTracker::end()) }
/** Gets a node that contains a MySQL pool created using `mysql.createPool()`. */
API::Node pool() {
result = createPool().getReturn()
or
result = pool().getMember("on").getReturn()
or
result = API::Node::ofType(moduleName(), ["Pool", "PoolCluster"])
}
/** Gets a call to `mysql.createConnection`. */
DataFlow::CallNode createConnection() { result = mysql().getAMemberCall("createConnection") }
/** Gets a data flow node that contains a freshly created MySQL connection instance. */
API::Node connection() {
result = createConnection().getReturn()
or
result = createConnection().getReturn().getPromised()
or
result = pool().getMember("getConnection").getParameter(0).getParameter(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)
/** Gets a reference to a MySQL connection instance. */
private DataFlow::SourceNode connection(DataFlow::TypeTracker t) {
t.start() and
(
result = createConnection()
or
result = pool().getAMethodCall("getConnection").getABoundCallbackParameter(0, 1)
)
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. */
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() {
exists(API::Node recv | recv = pool() or recv = connection() |
this = recv.getMember(["query", "execute"]).getACall()
)
}
QueryCall() { this = [pool(), connection()].getAMethodCall("query") }
override DataFlow::Node getAQueryArgument() { result = getArgument(0) }
}
@@ -88,7 +72,7 @@ private module MySql {
/** A call to the `escape` or `escapeId` method that performs SQL sanitization. */
class EscapingSanitizer extends SQL::SqlSanitizer, MethodCallExpr {
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
output = this
}
@@ -99,9 +83,8 @@ private module MySql {
string kind;
Credentials() {
exists(API::Node callee, string prop |
callee in [createConnection(), createPool()] and
this = callee.getParameter(0).getMember(prop).getARhs().asExpr() and
exists(string prop |
this = [createConnection(), createPool()].getOptionArgument(0, prop).asExpr() and
(
prop = "user" and kind = "user name"
or
@@ -118,61 +101,23 @@ private module MySql {
* Provides classes modelling the PostgreSQL packages, such as `pg` and `pg-promise`.
*/
private module Postgres {
API::Node pg() {
result = API::moduleImport("pg")
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() {
/** Gets an expression that constructs a new connection pool. */
DataFlow::InvokeNode newPool() {
// new require('pg').Pool()
result = pg().getMember("Pool")
result = DataFlow::moduleImport("pg").getAConstructorInvocation("Pool")
or
// 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. */
API::Node pool() {
result = newPool().getInstance()
or
result = pgpDatabase().getMember("$pool")
or
result = pool().getMember("on").getReturn()
or
result = API::Node::ofType("pg", "Pool")
/** Gets a creation of a Postgres client. */
DataFlow::InvokeNode newClient() {
result = DataFlow::moduleImport("pg").getAConstructorInvocation("Client")
}
/** A call to the Postgres `query` method. */
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() { this = [client(), pool()].getMember("query").getACall() }
QueryCall() { this = [newClient(), newPool()].getAMethodCall("query") }
override DataFlow::Node getAQueryArgument() { result = getArgument(0) }
}
@@ -191,11 +136,7 @@ private module Postgres {
string kind;
Credentials() {
exists(string prop |
this = [newClient(), newPool()].getParameter(0).getMember(prop).getARhs().asExpr()
or
this = pgPromise().getParameter(0).getMember(prop).getARhs().asExpr()
|
exists(string prop | this = [newClient(), newPool()].getOptionArgument(0, prop).asExpr() |
prop = "user" and kind = "user name"
or
prop = "password" and kind = prop
@@ -335,35 +276,30 @@ private module Postgres {
*/
private module Sqlite {
/** Gets a reference to the `sqlite3` module. */
API::Node sqlite() {
result = API::moduleImport("sqlite3")
DataFlow::SourceNode sqlite() {
result = DataFlow::moduleImport("sqlite3")
or
result = sqlite().getMember("verbose").getReturn()
result = sqlite().getAMemberCall("verbose")
}
/** Gets an expression that constructs or returns a Sqlite database instance. */
API::Node database() {
/** Gets an expression that constructs a Sqlite database instance. */
DataFlow::SourceNode newDb() {
// new require('sqlite3').Database()
result = sqlite().getMember("Database").getInstance()
or
// chained call
result = getAChainingQueryCall()
or
result = API::Node::ofType("sqlite3", "Database")
result = sqlite().getAConstructorInvocation("Database")
}
/** A call to a query method on a Sqlite database instance that returns the same instance. */
private API::Node getAChainingQueryCall() {
result = database().getMember(["all", "each", "exec", "get", "run"]).getReturn()
/** Gets a data flow node referring to a Sqlite database instance. */
private DataFlow::SourceNode db(DataFlow::TypeTracker t) {
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. */
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() {
this = getAChainingQueryCall().getAnImmediateUse()
or
this = database().getMember("prepare").getACall()
}
QueryCall() { this = db().getAMethodCall(["all", "each", "exec", "get", "prepare", "run"]) }
override DataFlow::Node getAQueryArgument() { result = getArgument(0) }
}
@@ -373,276 +309,3 @@ private module Sqlite {
QueryString() { this = any(QueryCall qc).getAQueryArgument().asExpr() }
}
}
/**
* Provides classes modelling 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 {
override TaggedTemplateExpr astNode;
QueryTemplateExpr() {
mssql().getMember("query").getAUse() = DataFlow::valueNode(astNode.getTag())
}
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 getAQueryArgument() { result = 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 modelling the `sequelize` package.
*/
private module Sequelize {
/** Gets an import of the `sequelize` module or one that re-exports it. */
API::Node sequelize() { result = API::moduleImport(["sequelize", "sequelize-typescript"]) }
/** Gets an expression that creates an instance of the `Sequelize` class. */
API::Node instance() {
result = [sequelize(), sequelize().getMember("Sequelize")].getInstance()
or
result = API::Node::ofType(["sequelize", "sequelize-typescript"], ["Sequelize", "default"])
}
/** A call to `Sequelize.query`. */
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() { this = instance().getMember("query").getACall() }
override DataFlow::Node getAQueryArgument() {
result = getArgument(0)
or
result = getOptionArgument(0, "query")
}
}
/** An expression that is passed to `Sequelize.query` method and hence interpreted as SQL. */
class QueryString extends SQL::SqlString {
QueryString() {
this = any(QueryCall qc).getAQueryArgument().asExpr()
or
this = sequelize().getMember(["literal", "asIs"]).getParameter(0).getARhs().asExpr()
}
}
/**
* An expression that is passed as user name or password when creating an instance of the
* `Sequelize` class.
*/
class Credentials extends CredentialsExpr {
string kind;
Credentials() {
exists(NewExpr ne, string prop |
ne = sequelize().getAnInstantiation().asExpr() and
(
this = ne.getArgument(1) and prop = "username"
or
this = ne.getArgument(2) and prop = "password"
or
ne.hasOptionArgument(ne.getNumArgument() - 1, prop, this)
) and
(
prop = "username" and kind = "user name"
or
prop = "password" and kind = prop
)
)
}
override string getCredentialsKind() { result = kind }
}
}
/**
* Provides classes modelling the Google Cloud Spanner library.
*/
private module Spanner {
/**
* Gets a node that refers to the `Spanner` class
*/
API::Node spanner() {
// older versions
result = API::moduleImport("@google-cloud/spanner")
or
// newer versions
result = API::moduleImport("@google-cloud/spanner").getMember("Spanner")
}
/**
* Gets a node that refers to an instance of the `Database` class.
*/
API::Node database() {
result =
spanner().getReturn().getMember("instance").getReturn().getMember("database").getReturn()
or
result = API::Node::ofType("@google-cloud/spanner", "Database")
}
/**
* Gets a node that refers to an instance of the `v1.SpannerClient` class.
*/
API::Node v1SpannerClient() {
result = spanner().getMember("v1").getMember("SpannerClient").getInstance()
or
result = API::Node::ofType("@google-cloud/spanner", "v1.SpannerClient")
}
/**
* Gets a node that refers to a transaction object.
*/
API::Node transaction() {
result =
database()
.getMember(["runTransaction", "runTransactionAsync"])
.getParameter([0, 1])
.getParameter(1)
or
result = API::Node::ofType("@google-cloud/spanner", "Transaction")
}
/** Gets an API node referring to a `BatchTransaction` object. */
API::Node batchTransaction() {
result = database().getMember("batchTransaction").getReturn()
or
result = database().getMember("createBatchTransaction").getReturn().getPromised()
or
result = API::Node::ofType("@google-cloud/spanner", "BatchTransaction")
}
/**
* A call to a Spanner method that executes a SQL query.
*/
abstract class SqlExecution extends DatabaseAccess, DataFlow::InvokeNode { }
/**
* A SQL execution that takes the input directly in the first argument or in the `sql` option.
*/
class SqlExecutionDirect extends SqlExecution {
SqlExecutionDirect() {
this = database().getMember(["run", "runPartitionedUpdate", "runStream"]).getACall()
or
this = transaction().getMember(["run", "runStream", "runUpdate"]).getACall()
or
this = batchTransaction().getMember("createQueryPartitions").getACall()
}
override DataFlow::Node getAQueryArgument() {
result = getArgument(0)
or
result = getOptionArgument(0, "sql")
}
}
/**
* A SQL execution that takes an array of SQL strings or { sql: string } objects.
*/
class SqlExecutionBatch extends SqlExecution, API::CallNode {
SqlExecutionBatch() { this = transaction().getMember("batchUpdate").getACall() }
override DataFlow::Node getAQueryArgument() {
// just use the whole array as the query argument, as arrays becomes tainted if one of the elements
// are tainted
result = getArgument(0)
or
result = getParameter(0).getUnknownMember().getMember("sql").getARhs()
}
}
/**
* A SQL execution that only takes the input in the `sql` option, and do not accept query strings
* directly.
*/
class SqlExecutionWithOption extends SqlExecution {
SqlExecutionWithOption() {
this = v1SpannerClient().getMember(["executeSql", "executeStreamingSql"]).getACall()
}
override DataFlow::Node getAQueryArgument() { result = getOptionArgument(0, "sql") }
}
/**
* An expression that is interpreted as a SQL string.
*/
class QueryString extends SQL::SqlString {
QueryString() { this = any(SqlExecution se).getAQueryArgument().asExpr() }
}
}

View File

@@ -36,25 +36,7 @@ module ParseTorrent {
* An access to user-controlled torrent information.
*/
class UserControlledTorrentInfo extends RemoteFlowSource {
UserControlledTorrentInfo() {
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())
)
}
UserControlledTorrentInfo() { none() }
override string getSourceType() { result = "torrent information" }
}

View File

@@ -484,8 +484,6 @@ module JQuery {
private DataFlow::SourceNode dollar(DataFlow::TypeTracker t) {
t.start() and
result = dollarSource()
or
exists(DataFlow::TypeTracker t2 | result = dollar(t2).track(t2, t))
}
/**
@@ -519,14 +517,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. */

View File

@@ -229,14 +229,8 @@ module CodeInjection {
}
/**
* A code operator of a NoSQL query 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.
* The first argument to `Module.prototype._compile` from the Node.js built-in module `module`,
* considered as a code-injection sink.
*/
class ModuleCompileSink extends Sink {
ModuleCompileSink() {

View File

@@ -55,7 +55,7 @@ module TaintedPath {
* There are currently four flow labels, representing the different combinations of
* normalization and absoluteness.
*/
abstract class PosixPath extends DataFlow::FlowLabel {
class PosixPath extends DataFlow::FlowLabel {
Normalization normalization;
Relativeness relativeness;
@@ -113,7 +113,7 @@ module TaintedPath {
/**
* 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" }
}
}
@@ -218,12 +218,12 @@ module TaintedPath {
output = this
or
// non-global replace or replace of something other than /\.\./g, /[/]/g, or /[\.]/g.
this instanceof StringReplaceCall and
this.getCalleeName() = "replace" and
input = getReceiver() and
output = this and
not exists(RegExpLiteral literal, RegExpTerm term |
this.(StringReplaceCall).getRegExp().asExpr() = literal and
this.(StringReplaceCall).isGlobal() and
getArgument(0).getALocalSource().asExpr() = literal and
literal.isGlobal() and
literal.getRoot() = term
|
term.getAMatchedString() = "/" or
@@ -247,15 +247,16 @@ module TaintedPath {
/**
* 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 output;
DotDotSlashPrefixRemovingReplace() {
this.getCalleeName() = "replace" and
input = getReceiver() and
output = this and
exists(RegExpLiteral literal, RegExpTerm term |
getRegExp().asExpr() = literal and
getArgument(0).getALocalSource().asExpr() = literal and
(term instanceof RegExpStar or term instanceof RegExpPlus) and
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.
*/
class DotRemovingReplaceCall extends StringReplaceCall {
class DotRemovingReplaceCall extends DataFlow::CallNode {
DataFlow::Node input;
DataFlow::Node output;
DotRemovingReplaceCall() {
this.getCalleeName() = "replace" and
input = getReceiver() and
output = this and
isGlobal() and
exists(RegExpLiteral literal, RegExpTerm term |
getRegExp().asExpr() = literal and
getArgument(0).getALocalSource().asExpr() = literal and
literal.isGlobal() and
literal.getRoot() = term and
not term.getAMatchedString() = "/"
|
@@ -606,8 +608,6 @@ module TaintedPath {
(
this = fileSystemAccess.getAPathArgument() and
not exists(fileSystemAccess.getRootPathArgument())
or
this = fileSystemAccess.getRootPathArgument()
) and
not this = any(ResolvingPathCall call).getInput()
}
@@ -648,74 +648,6 @@ module TaintedPath {
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.
*/

View File

@@ -183,17 +183,6 @@ module DomBasedXss {
this = any(Typeahead::TypeaheadSuggestionFunction f).getAReturn()
or
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)
}
}