Compare commits

...

16 Commits

Author SHA1 Message Date
Max Schaefer
f5a6d31227 Revert "JS: Recognize DomSanitizer from @angular/core"
This reverts commit ff1d0cc4c7.
2022-03-28 16:02:01 +01:00
Henry Mercer
9b675913da Remove NoSQL sinks since September 2018 2022-03-28 16:02:01 +01:00
Esben Sparre Andreasen
9b9c19b76c Remove additional Xss sinks 2022-03-28 16:01:58 +01:00
Esben Sparre Andreasen
ab9c78678c Remove additional SQL sinks 2022-03-28 16:01:32 +01:00
Esben Sparre Andreasen
f219d2b745 Remove additional path-injection sinks 2022-03-28 16:01:10 +01:00
Esben Sparre Andreasen
c7b601e61a Remove pseudo-properties 2022-03-28 16:00:45 +01:00
Esben Sparre Andreasen
3052798b01 Remove 2020 sinks from SqlInjection.ql 2022-03-28 16:00:45 +01:00
Esben Sparre Andreasen
4b86ede1b4 Remove 2020 sinks from Xss.ql 2022-03-28 16:00:45 +01:00
Esben Sparre Andreasen
49f215c7a1 Remove 2020 sinks from TaintedPath.ql 2022-03-28 16:00:45 +01:00
Arthur Baars
8fe48e1084 Ruby: drop unused predicates that do not exist in Python variant 2022-03-28 16:00:45 +01:00
Arthur Baars
0350e7a47b Apply suggestions from code review
Co-authored-by: Nick Rolfe <nickrolfe@github.com>
2022-03-28 16:00:45 +01:00
Arthur Baars
c9a50f87e8 Ruby: move RegExpTreeView.qll out of 'internal' 2022-03-28 16:00:44 +01:00
Arthur Baars
648684e38a Apply suggestions from code review
Co-authored-by: yoff <lerchedahl@gmail.com>
2022-03-28 16:00:44 +01:00
Arthur Baars
5af3a0e5b4 Add change note 2022-03-28 16:00:44 +01:00
Arthur Baars
f518f0ab28 Ruby: make ParseRegExp.qll and RegExpTreeView.qll internal libraries 2022-03-28 16:00:44 +01:00
Arthur Baars
717490cb1f Ruby: refactor regex libraries 2022-03-28 16:00:44 +01:00
29 changed files with 1481 additions and 2494 deletions

View File

@@ -354,35 +354,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))
}
}
}
@@ -416,11 +387,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

@@ -183,12 +183,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

@@ -756,10 +756,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

@@ -927,28 +927,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" |
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.
*/

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.
*/
@@ -118,319 +101,3 @@ private API::Node fastGlobFileName() {
private class FastGlobFileNameSource extends FileNameSource {
FastGlobFileNameSource() { this = fastGlobFileName().getAnImmediateUse() }
}
/**
* 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, API::CallNode {
RecursiveReadDir() { this = API::moduleImport("recursive-readdir").getACall() }
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
override DataFlow::Node getAFileName() { result = this.trackFileSource().getAnImmediateUse() }
private API::Node trackFileSource() {
result = this.getParameter([1 .. 2]).getParameter(1)
or
not exists(this.getCallback([1 .. 2])) and result = this.getReturn().getPromised()
}
}
/**
* 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, API::CallNode {
JsonFileReader() {
this = API::moduleImport("jsonfile").getMember(["readFile", "readFileSync"]).getACall()
}
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
override DataFlow::Node getADataNode() { result = this.trackRead().getAnImmediateUse() }
private API::Node trackRead() {
this.getCalleeName() = "readFile" and
(
result = this.getParameter([1 .. 2]).getParameter(1)
or
not exists(this.getCallback([1 .. 2])) and result = this.getReturn().getPromised()
)
or
this.getCalleeName() = "readFileSync" and
result = this.getReturn()
}
}
/** DEPRECATED: Alias for JsonFileReader */
deprecated class JSONFileReader = JsonFileReader;
/**
* 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) }
}
/** DEPRECATED: Alias for JsonFileWriter */
deprecated class JSONFileWriter = JsonFileWriter;
}
/**
* A call to the library `load-json-file`.
*/
private class LoadJsonFile extends FileSystemReadAccess, API::CallNode {
LoadJsonFile() {
this = API::moduleImport("load-json-file").getACall()
or
this = API::moduleImport("load-json-file").getMember("sync").getACall()
}
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
override DataFlow::Node getADataNode() { result = this.trackRead().getAnImmediateUse() }
private API::Node trackRead() {
this.getCalleeName() = "sync" and result = this.getReturn()
or
not this.getCalleeName() = "sync" and result = this.getReturn().getPromised()
}
}
/**
* 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, API::CallNode {
WalkDir() {
this = API::moduleImport("walkdir").getACall()
or
this = API::moduleImport("walkdir").getMember("sync").getACall()
or
this = API::moduleImport("walkdir").getMember("async").getACall()
}
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
override DataFlow::Node getAFileName() { result = this.trackFileSource().getAnImmediateUse() }
private API::Node trackFileSource() {
not this.getCalleeName() = ["sync", "async"] and
(
result = this.getParameter(this.getNumArgument() - 1).getParameter(0)
or
result = this.getReturn().getMember(EventEmitter::on()).getParameter(1).getParameter(0)
)
or
this.getCalleeName() = "sync" and result = this.getReturn()
or
this.getCalleeName() = "async" and result = this.getReturn().getPromised()
}
}
/**
* 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) }
}

View File

@@ -17,135 +17,117 @@ module NoSql {
/** DEPRECATED: Alias for NoSql */
deprecated module NoSQL = NoSql;
/**
* 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()
}
/**
* Provides classes modeling the MongoDB library.
*/
private module MongoDB {
/**
* Gets an access to `mongodb.MongoClient` or a database.
*
* 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.
* Gets an import of MongoDB.
*/
private API::Node getAMongoClientOrDatabase() {
result = API::moduleImport("mongodb").getMember("MongoClient")
DataFlow::ModuleImportNode mongodb() { result.getPath() = "mongodb" }
/**
* Gets an access to `mongodb.MongoClient`.
*/
private DataFlow::SourceNode getAMongoClient(DataFlow::TypeTracker t) {
t.start() and
result = mongodb().getAPropertyRead("MongoClient")
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
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. */
private API::Node getACollection() {
// A collection resulting from calling `Db.collection(...)`.
exists(API::Node collection |
collection = getAMongoClientOrDatabase().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 = this.getArgument(queryArgIdx) }
override DataFlow::Node getAResult() {
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
}
DataFlow::Node getACodeOperator() {
result = getADollarWhereProperty(this.getParameter(queryArgIdx))
}
override DataFlow::Node getAQueryArgument() { result = getArgument(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,342 +138,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", "init", "populate", "overwrite"]
}
}
class Model extends MongoDB::Collection {
Model() { this = getAMongooseInstance().getAMemberCall("model") }
}
/**
@@ -501,9 +161,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"
@@ -512,308 +170,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
}
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() }
}
}

View File

@@ -493,56 +493,11 @@ module NodeJSLib {
*/
module FS {
/**
* Gets 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)
)
}
}
@@ -553,7 +508,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

@@ -33,54 +33,38 @@ module SQL {
* Provides classes modeling 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 getAResult() { result = this.getCallback(_).getParameter(1) }
@@ -97,7 +81,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
}
@@ -108,9 +92,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
@@ -127,61 +110,23 @@ private module MySql {
* Provides classes modeling 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 getAResult() {
this.getNumArgument() = 2 and
@@ -210,11 +155,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
@@ -359,35 +300,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")
}
/** Gets 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 getAResult() {
result = this.getCallback(1).getParameter(1) or
@@ -402,203 +338,3 @@ private module Sqlite {
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"
}
}
}

View File

@@ -34,25 +34,8 @@ module ParseTorrent {
/**
* An access to user-controlled torrent information.
*/
class UserControlledTorrentInfo extends RemoteFlowSource instanceof DataFlow::PropRead {
UserControlledTorrentInfo() {
exists(API::Node read |
read = any(ParsedTorrent t).asApiNode().getAMember() and
this = read.getAnImmediateUse()
|
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
super.getPropertyName() = prop
)
or
not exists(super.getPropertyName())
)
}
class UserControlledTorrentInfo extends RemoteFlowSource {
UserControlledTorrentInfo() { none() }
override string getSourceType() { result = "torrent information" }
}

View File

@@ -428,8 +428,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))
}
/**
@@ -463,14 +461,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())
}
}
}
/** Gets a source of jQuery objects from the AST-based `JQueryObject` class. */

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
input = this.getReceiver() 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() {
input = this.getReceiver() and
this.getCalleeName() = "replace" and
input = getReceiver() and
output = this and
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.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() {
input = this.getReceiver() and
this.getCalleeName() = "replace" and
input = getReceiver() and
output = this and
this.isGlobal() and
exists(RegExpLiteral literal, RegExpTerm term |
this.getRegExp().asExpr() = literal and
getArgument(0).getALocalSource().asExpr() = literal and
literal.isGlobal() and
literal.getRoot() = term and
not term.getAMatchedString() = "/"
|
@@ -622,8 +624,6 @@ module TaintedPath {
(
this = fileSystemAccess.getAPathArgument() and
not exists(fileSystemAccess.getRootPathArgument())
or
this = fileSystemAccess.getRootPathArgument()
) and
not this = any(ResolvingPathCall call).getInput()
}
@@ -664,74 +664,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

@@ -186,17 +186,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)
}
}

View File

@@ -552,7 +552,7 @@ class RegExpWordBoundary extends RegExpSpecialChar {
/**
* A character class escape in a regular expression.
* That is, an escaped charachter that denotes multiple characters.
* That is, an escaped character that denotes multiple characters.
*
* Examples:
*

View File

@@ -188,7 +188,7 @@ abstract class RegexString extends Expr {
)
}
/** Hold is a character set starts between `start` and `end`. */
/** Holds if a character set starts between `start` and `end`. */
predicate char_set_start(int start, int end) {
this.char_set_start(start) = true and
(
@@ -316,8 +316,10 @@ abstract class RegexString extends Expr {
result = this.(Bytes).getS()
}
/** Gets the `i`th character of this regex */
string getChar(int i) { result = this.getText().charAt(i) }
/** Gets the `i`th character of this regex, unless it is part of a character escape sequence. */
string nonEscapedCharAt(int i) {
result = this.getText().charAt(i) and
not exists(int x, int y | this.escapedCharacter(x, y) and i in [x .. y - 1])
@@ -329,6 +331,9 @@ abstract class RegexString extends Expr {
private predicate isGroupStart(int i) { this.nonEscapedCharAt(i) = "(" and not this.inCharSet(i) }
/**
* Holds if the `i`th character could not be parsed.
*/
predicate failedToParse(int i) {
exists(this.getChar(i)) and
not exists(int start, int end |
@@ -417,6 +422,9 @@ abstract class RegexString extends Expr {
)
}
/**
* Holds if a simple or escaped character is found between `start` and `end`.
*/
predicate character(int start, int end) {
(
this.simpleCharacter(start, end) and
@@ -428,12 +436,18 @@ abstract class RegexString extends Expr {
not exists(int x, int y | this.backreference(x, y) and x <= start and y >= end)
}
/**
* Holds if a normal character is found between `start` and `end`.
*/
predicate normalCharacter(int start, int end) {
end = start + 1 and
this.character(start, end) and
not this.specialCharacter(start, end, _)
}
/**
* Holds if a special character is found between `start` and `end`.
*/
predicate specialCharacter(int start, int end, string char) {
not this.inCharSet(start) and
this.character(start, end) and
@@ -492,7 +506,7 @@ abstract class RegexString extends Expr {
this.specialCharacter(start, end, _)
}
/** Whether the text in the range start,end is a group */
/** Whether the text in the range `start,end` is a group */
predicate group(int start, int end) {
this.groupContents(start, end, _, _)
or
@@ -611,6 +625,7 @@ abstract class RegexString extends Expr {
this.simple_group_start(start, end)
}
/** Matches the start of a non-capturing group, e.g. `(?:` */
private predicate non_capturing_group_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
@@ -618,12 +633,18 @@ abstract class RegexString extends Expr {
end = start + 3
}
/** Matches the start of a simple group, e.g. `(a+)`. */
private predicate simple_group_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) != "?" and
end = start + 1
}
/**
* Matches the start of a named group, such as:
* - `(?<name>\w+)`
* - `(?'name'\w+)`
*/
private predicate named_group_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
@@ -675,6 +696,7 @@ abstract class RegexString extends Expr {
)
}
/** Matches the start of a positive lookahead assertion, i.e. `(?=`. */
private predicate lookahead_assertion_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
@@ -682,6 +704,7 @@ abstract class RegexString extends Expr {
end = start + 3
}
/** Matches the start of a negative lookahead assertion, i.e. `(?!`. */
private predicate negative_lookahead_assertion_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
@@ -689,6 +712,7 @@ abstract class RegexString extends Expr {
end = start + 3
}
/** Matches the start of a positive lookbehind assertion, i.e. `(?<=`. */
private predicate lookbehind_assertion_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
@@ -697,6 +721,7 @@ abstract class RegexString extends Expr {
end = start + 4
}
/** Matches the start of a negative lookbehind assertion, i.e. `(?<!`. */
private predicate negative_lookbehind_assertion_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
@@ -705,6 +730,7 @@ abstract class RegexString extends Expr {
end = start + 4
}
/** Matches the start of a comment group, i.e. `(?#`. */
private predicate comment_group_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
@@ -712,6 +738,7 @@ abstract class RegexString extends Expr {
end = start + 3
}
/** Matches the contents of a group. */
predicate groupContents(int start, int end, int in_start, int in_end) {
this.group_start(start, in_start) and
end = in_end + 1 and
@@ -719,12 +746,14 @@ abstract class RegexString extends Expr {
this.isGroupEnd(in_end)
}
/** Matches a named backreference, e.g. `\k<foo>`. */
private predicate named_backreference(int start, int end, string name) {
this.named_backreference_start(start, start + 4) and
end = min(int i | i > start + 4 and this.getChar(i) = ")") + 1 and
name = this.getText().substring(start + 4, end - 2)
}
/** Matches a numbered backreference, e.g. `\1`. */
private predicate numbered_backreference(int start, int end, int value) {
this.escapingChar(start) and
// starting with 0 makes it an octal escape
@@ -749,7 +778,7 @@ abstract class RegexString extends Expr {
)
}
/** Whether the text in the range start,end is a back reference */
/** Whether the text in the range `start,end` is a back reference */
predicate backreference(int start, int end) {
this.numbered_backreference(start, end, _)
or

View File

@@ -1,4 +1,4 @@
import codeql.ruby.security.performance.RegExpTreeView
import codeql.ruby.Regexp
query predicate nonUniqueChild(RegExpParent parent, int i, RegExpTerm child) {
child = parent.getChild(i) and

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The `ParseRegExp` and `RegExpTreeView` modules are now "internal" modules. Users should use `codeql.ruby.Regexp` instead.

View File

@@ -0,0 +1,143 @@
/**
* Provides classes for working with regular expressions.
*
* Regular expression literals are represented as an abstract syntax tree of regular expression
* terms.
*/
import regexp.RegExpTreeView // re-export
private import regexp.internal.ParseRegExp
private import codeql.ruby.ast.Literal as AST
private import codeql.ruby.DataFlow
private import codeql.ruby.controlflow.CfgNodes
private import codeql.ruby.ApiGraphs
private import codeql.ruby.dataflow.internal.tainttrackingforlibraries.TaintTrackingImpl
/**
* Provides utility predicates related to regular expressions.
*/
module RegExpPatterns {
/**
* Gets a pattern that matches common top-level domain names in lower case.
*/
string getACommonTld() {
// according to ranking by http://google.com/search?q=site:.<<TLD>>
result = "(?:com|org|edu|gov|uk|net|io)(?![a-z0-9])"
}
}
/**
* A node whose value may flow to a position where it is interpreted
* as a part of a regular expression.
*/
abstract class RegExpPatternSource extends DataFlow::Node {
/**
* Gets a node where the pattern of this node is parsed as a part of
* a regular expression.
*/
abstract DataFlow::Node getAParse();
/**
* Gets the root term of the regular expression parsed from this pattern.
*/
abstract RegExpTerm getRegExpTerm();
}
/**
* A regular expression literal, viewed as the pattern source for itself.
*/
private class RegExpLiteralPatternSource extends RegExpPatternSource {
private AST::RegExpLiteral astNode;
RegExpLiteralPatternSource() { astNode = this.asExpr().getExpr() }
override DataFlow::Node getAParse() { result = this }
override RegExpTerm getRegExpTerm() { result = astNode.getParsed() }
}
/**
* A node whose string value may flow to a position where it is interpreted
* as a part of a regular expression.
*/
private class StringRegExpPatternSource extends RegExpPatternSource {
private DataFlow::Node parse;
StringRegExpPatternSource() { this = regExpSource(parse) }
override DataFlow::Node getAParse() { result = parse }
override RegExpTerm getRegExpTerm() { result.getRegExp() = this.asExpr().getExpr() }
}
private class RegExpLiteralRegExp extends RegExp, AST::RegExpLiteral {
override predicate isDotAll() { this.hasMultilineFlag() }
override predicate isIgnoreCase() { this.hasCaseInsensitiveFlag() }
override string getFlags() { result = this.getFlagString() }
}
private class ParsedStringRegExp extends RegExp {
private DataFlow::Node parse;
ParsedStringRegExp() { this = regExpSource(parse).asExpr().getExpr() }
DataFlow::Node getAParse() { result = parse }
override predicate isDotAll() { none() }
override predicate isIgnoreCase() { none() }
override string getFlags() { none() }
}
/**
* Holds if `source` may be interpreted as a regular expression.
*/
private predicate isInterpretedAsRegExp(DataFlow::Node source) {
// The first argument to an invocation of `Regexp.new` or `Regexp.compile`.
source = API::getTopLevelMember("Regexp").getAMethodCall(["compile", "new"]).getArgument(0)
or
// The argument of a call that coerces the argument to a regular expression.
exists(DataFlow::CallNode mce |
mce.getMethodName() = ["match", "match?"] and
source = mce.getArgument(0) and
// exclude https://ruby-doc.org/core-2.4.0/Regexp.html#method-i-match
not mce.getReceiver().asExpr().getExpr() instanceof AST::RegExpLiteral
)
}
private class RegExpConfiguration extends Configuration {
RegExpConfiguration() { this = "RegExpConfiguration" }
override predicate isSource(DataFlow::Node source) {
source.asExpr() =
any(ExprCfgNode e |
e.getConstantValue().isString(_) and
not e instanceof ExprNodes::VariableReadAccessCfgNode and
not e instanceof ExprNodes::ConstantReadAccessCfgNode
)
}
override predicate isSink(DataFlow::Node sink) { isInterpretedAsRegExp(sink) }
override predicate isSanitizer(DataFlow::Node node) {
// stop flow if `node` is receiver of
// https://ruby-doc.org/core-2.4.0/String.html#method-i-match
exists(DataFlow::CallNode mce |
mce.getMethodName() = ["match", "match?"] and
node = mce.getReceiver() and
mce.getArgument(0).asExpr().getExpr() instanceof AST::RegExpLiteral
)
}
}
/**
* Gets a node whose value may flow (inter-procedurally) to `re`, where it is interpreted
* as a part of a regular expression.
*/
cached
DataFlow::Node regExpSource(DataFlow::Node re) {
exists(RegExpConfiguration c | c.hasFlow(result, re))
}

View File

@@ -1,5 +1,5 @@
private import codeql.ruby.AST
private import codeql.ruby.security.performance.RegExpTreeView as RETV
private import codeql.ruby.Regexp as RE
private import internal.AST
private import internal.Constant
private import internal.Literal
@@ -393,7 +393,7 @@ class RegExpLiteral extends StringlikeLiteral instanceof RegExpLiteralImpl {
final predicate hasFreeSpacingFlag() { this.getFlagString().charAt(_) = "x" }
/** Returns the root node of the parse tree of this regular expression. */
final RETV::RegExpTerm getParsed() { result = RETV::getParsedRegExp(this) }
final RE::RegExpTerm getParsed() { result = RE::getParsedRegExp(this) }
}
/**

View File

@@ -7,7 +7,7 @@
*/
private import AST
private import codeql.ruby.security.performance.RegExpTreeView as RETV
private import codeql.ruby.Regexp as RE
private import codeql.ruby.ast.internal.Synthesis
/**
@@ -37,7 +37,7 @@ private predicate shouldPrintAstEdge(AstNode parent, string edgeName, AstNode ch
newtype TPrintNode =
TPrintRegularAstNode(AstNode n) { shouldPrintNode(n) } or
TPrintRegExpNode(RETV::RegExpTerm term) {
TPrintRegExpNode(RE::RegExpTerm term) {
exists(RegExpLiteral literal |
shouldPrintNode(literal) and
term.getRootTerm() = literal.getParsed()
@@ -107,7 +107,7 @@ class PrintRegularAstNode extends PrintAstNode, TPrintRegularAstNode {
or
// If this AST node is a regexp literal, add the parsed regexp tree as a
// child.
exists(RETV::RegExpTerm t | t = astNode.(RegExpLiteral).getParsed() |
exists(RE::RegExpTerm t | t = astNode.(RegExpLiteral).getParsed() |
result = TPrintRegExpNode(t) and edgeName = "getParsed"
)
}
@@ -134,7 +134,7 @@ class PrintRegularAstNode extends PrintAstNode, TPrintRegularAstNode {
/** A parsed regexp node in the output tree. */
class PrintRegExpNode extends PrintAstNode, TPrintRegExpNode {
RETV::RegExpTerm regexNode;
RE::RegExpTerm regexNode;
PrintRegExpNode() { this = TPrintRegExpNode(regexNode) }
@@ -147,7 +147,7 @@ class PrintRegExpNode extends PrintAstNode, TPrintRegExpNode {
exists(int i | result = TPrintRegExpNode(regexNode.getChild(i)) and edgeName = i.toString())
}
override int getOrder() { exists(RETV::RegExpTerm p | p.getChild(result) = regexNode) }
override int getOrder() { exists(RE::RegExpTerm p | p.getChild(result) = regexNode) }
override predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn

File diff suppressed because it is too large Load Diff

View File

@@ -7,10 +7,6 @@
private import codeql.ruby.ast.Literal as AST
private import codeql.Locations
private import codeql.ruby.DataFlow
private import codeql.ruby.controlflow.CfgNodes
private import codeql.ruby.ApiGraphs
private import codeql.ruby.dataflow.internal.tainttrackingforlibraries.TaintTrackingImpl
/**
* A `StringlikeLiteral` containing a regular expression term, that is, either
@@ -116,6 +112,7 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/** Holds if a character set starts between `start` and `end`. */
predicate charSetStart(int start, int end) {
this.charSetStart(start) = true and
(
@@ -145,14 +142,21 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
predicate charSetToken(int charsetStart, int index, int tokenStart, int tokenEnd) {
/**
* Holds if the character set starting at `charsetStart` contains either
* a character or a `-` found between `start` and `end`.
*/
private predicate charSetToken(int charsetStart, int index, int tokenStart, int tokenEnd) {
tokenStart =
rank[index](int start, int end | this.charSetToken(charsetStart, start, end) | start) and
this.charSetToken(charsetStart, tokenStart, tokenEnd)
}
/** Either a char or a - */
predicate charSetToken(int charsetStart, int start, int end) {
/**
* Holds if the character set starting at `charsetStart` contains either
* a character or a `-` found between `start` and `end`.
*/
private predicate charSetToken(int charsetStart, int start, int end) {
this.charSetStart(charsetStart, start) and
(
this.escapedCharacter(start, end)
@@ -174,6 +178,10 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/**
* Holds if the character set starting at `charsetStart` contains either
* a character or a range found between `start` and `end`.
*/
predicate charSetChild(int charsetStart, int start, int end) {
this.charSetToken(charsetStart, start, end) and
not exists(int rangeStart, int rangeEnd |
@@ -185,6 +193,11 @@ abstract class RegExp extends AST::StringlikeLiteral {
this.charRange(charsetStart, start, _, _, end)
}
/**
* Holds if the character set starting at `charset_start` contains a character range
* with lower bound found between `start` and `lower_end`
* and upper bound found between `upper_start` and `end`.
*/
predicate charRange(int charsetStart, int start, int lowerEnd, int upperStart, int end) {
exists(int index |
this.charRangeEnd(charsetStart, index) = true and
@@ -193,6 +206,13 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/**
* Helper predicate for `charRange`.
* We can determine where character ranges end by a left to right sweep.
*
* To avoid negative recursion we return a boolean. See `escaping`,
* the helper for `escapingChar`, for a clean use of this pattern.
*/
private boolean charRangeEnd(int charsetStart, int index) {
this.charSetToken(charsetStart, index, _, _) and
(
@@ -216,8 +236,15 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/** Holds if the character at `pos` is a "\" that is actually escaping what comes after. */
predicate escapingChar(int pos) { this.escaping(pos) = true }
/**
* Helper predicate for `escapingChar`.
* In order to avoid negative recusrion, we return a boolean.
* This way, we can refer to `escaping(pos - 1).booleanNot()`
* rather than to a negated version of `escaping(pos)`.
*/
private boolean escaping(int pos) {
pos = -1 and result = false
or
@@ -229,8 +256,10 @@ abstract class RegExp extends AST::StringlikeLiteral {
/** Gets the text of this regex */
string getText() { result = this.getConstantValue().getString() }
/** Gets the `i`th character of this regex */
string getChar(int i) { result = this.getText().charAt(i) }
/** Gets the `i`th character of this regex, unless it is part of a character escape sequence. */
string nonEscapedCharAt(int i) {
result = this.getText().charAt(i) and
not exists(int x, int y | this.escapedCharacter(x, y) and i in [x .. y - 1])
@@ -242,6 +271,9 @@ abstract class RegExp extends AST::StringlikeLiteral {
private predicate isGroupStart(int i) { this.nonEscapedCharAt(i) = "(" and not this.inCharSet(i) }
/**
* Holds if the `i`th character could not be parsed.
*/
predicate failedToParse(int i) {
exists(this.getChar(i)) and
not exists(int start, int end |
@@ -331,6 +363,11 @@ abstract class RegExp extends AST::StringlikeLiteral {
this.getChar(start + 3) = "^"
}
/**
* Holds if an escaped character is found between `start` and `end`.
* Escaped characters include hex values, octal values and named escapes,
* but excludes backreferences.
*/
predicate escapedCharacter(int start, int end) {
this.escapingChar(start) and
not this.numberedBackreference(start, _, _) and
@@ -350,17 +387,25 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/**
* Holds if the character at `index` is inside a character set.
*/
predicate inCharSet(int index) {
exists(int x, int y | this.charSet(x, y) and index in [x + 1 .. y - 2])
}
/**
* Holds if the character at `index` is inside a posix bracket.
*/
predicate inPosixBracket(int index) {
exists(int x, int y |
this.posixStyleNamedCharacterProperty(x, y, _) and index in [x + 1 .. y - 2]
)
}
/** 'Simple' characters are any that don't alter the parsing of the regex. */
/**
* 'simple' characters are any that don't alter the parsing of the regex.
*/
private predicate simpleCharacter(int start, int end) {
end = start + 1 and
not this.charSet(start, _) and
@@ -391,6 +436,9 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/**
* Holds if a simple or escaped character is found between `start` and `end`.
*/
predicate character(int start, int end) {
(
this.simpleCharacter(start, end) and
@@ -406,12 +454,18 @@ abstract class RegExp extends AST::StringlikeLiteral {
not exists(int x, int y | this.multiples(x, y, _, _) and x <= start and y >= end)
}
/**
* Holds if a normal character is found between `start` and `end`.
*/
predicate normalCharacter(int start, int end) {
end = start + 1 and
this.character(start, end) and
not this.specialCharacter(start, end, _)
}
/**
* Holds if a special character is found between `start` and `end`.
*/
predicate specialCharacter(int start, int end, string char) {
this.character(start, end) and
not this.inCharSet(start) and
@@ -505,6 +559,7 @@ abstract class RegExp extends AST::StringlikeLiteral {
this.positiveLookbehindAssertionGroup(start, end)
}
/** Holds if an empty group is found between `start` and `end`. */
predicate emptyGroup(int start, int end) {
exists(int endm1 | end = endm1 + 1 |
this.groupStart(start, endm1) and
@@ -538,24 +593,28 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/** Holds if a negative lookahead is found between `start` and `end` */
predicate negativeLookaheadAssertionGroup(int start, int end) {
exists(int inStart | this.negativeLookaheadAssertionStart(start, inStart) |
this.groupContents(start, end, inStart, _)
)
}
/** Holds if a negative lookbehind is found between `start` and `end` */
predicate negativeLookbehindAssertionGroup(int start, int end) {
exists(int inStart | this.negativeLookbehindAssertionStart(start, inStart) |
this.groupContents(start, end, inStart, _)
)
}
/** Holds if a positive lookahead is found between `start` and `end` */
predicate positiveLookaheadAssertionGroup(int start, int end) {
exists(int inStart | this.lookaheadAssertionStart(start, inStart) |
this.groupContents(start, end, inStart, _)
)
}
/** Holds if a positive lookbehind is found between `start` and `end` */
predicate positiveLookbehindAssertionGroup(int start, int end) {
exists(int inStart | this.lookbehindAssertionStart(start, inStart) |
this.groupContents(start, end, inStart, _)
@@ -661,6 +720,7 @@ abstract class RegExp extends AST::StringlikeLiteral {
end = start + 3
}
/** Matches the contents of a group. */
predicate groupContents(int start, int end, int inStart, int inEnd) {
this.groupStart(start, inStart) and
end = inEnd + 1 and
@@ -747,6 +807,11 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/**
* Holds if a repetition quantifier is found between `start` and `end`,
* with the given lower and upper bounds. If a bound is omitted, the corresponding
* string is empty.
*/
predicate multiples(int start, int end, string lower, string upper) {
exists(string text, string match, string inner |
text = this.getText() and
@@ -774,6 +839,13 @@ abstract class RegExp extends AST::StringlikeLiteral {
this.qualifiedPart(start, _, end, maybeEmpty, mayRepeatForever)
}
/**
* Holds if a qualified part is found between `start` and `part_end` and the qualifier is
* found between `part_end` and `end`.
*
* `maybe_empty` is true if the part is optional.
* `may_repeat_forever` is true if the part may be repeated unboundedly.
*/
predicate qualifiedPart(
int start, int partEnd, int end, boolean maybeEmpty, boolean mayRepeatForever
) {
@@ -781,6 +853,7 @@ abstract class RegExp extends AST::StringlikeLiteral {
this.qualifier(partEnd, end, maybeEmpty, mayRepeatForever)
}
/** Holds if the range `start`, `end` contains a character, a quantifier, a character set or a group. */
predicate item(int start, int end) {
this.qualifiedItem(start, end, _, _)
or
@@ -960,75 +1033,3 @@ abstract class RegExp extends AST::StringlikeLiteral {
this.lastPart(start, end)
}
}
private class RegExpLiteralRegExp extends RegExp, AST::RegExpLiteral {
override predicate isDotAll() { this.hasMultilineFlag() }
override predicate isIgnoreCase() { this.hasCaseInsensitiveFlag() }
override string getFlags() { result = this.getFlagString() }
}
private class ParsedStringRegExp extends RegExp {
private DataFlow::Node parse;
ParsedStringRegExp() { this = regExpSource(parse).asExpr().getExpr() }
DataFlow::Node getAParse() { result = parse }
override predicate isDotAll() { none() }
override predicate isIgnoreCase() { none() }
override string getFlags() { none() }
}
/**
* Holds if `source` may be interpreted as a regular expression.
*/
private predicate isInterpretedAsRegExp(DataFlow::Node source) {
// The first argument to an invocation of `Regexp.new` or `Regexp.compile`.
source = API::getTopLevelMember("Regexp").getAMethodCall(["compile", "new"]).getArgument(0)
or
// The argument of a call that coerces the argument to a regular expression.
exists(DataFlow::CallNode mce |
mce.getMethodName() = ["match", "match?"] and
source = mce.getArgument(0) and
// exclude https://ruby-doc.org/core-2.4.0/Regexp.html#method-i-match
not mce.getReceiver().asExpr().getExpr() instanceof AST::RegExpLiteral
)
}
private class RegExpConfiguration extends Configuration {
RegExpConfiguration() { this = "RegExpConfiguration" }
override predicate isSource(DataFlow::Node source) {
source.asExpr() =
any(ExprCfgNode e |
e.getConstantValue().isString(_) and
not e instanceof ExprNodes::VariableReadAccessCfgNode and
not e instanceof ExprNodes::ConstantReadAccessCfgNode
)
}
override predicate isSink(DataFlow::Node sink) { isInterpretedAsRegExp(sink) }
override predicate isSanitizer(DataFlow::Node node) {
// stop flow if `node` is receiver of
// https://ruby-doc.org/core-2.4.0/String.html#method-i-match
exists(DataFlow::CallNode mce |
mce.getMethodName() = ["match", "match?"] and
node = mce.getReceiver() and
mce.getArgument(0).asExpr().getExpr() instanceof AST::RegExpLiteral
)
}
}
/**
* Gets a node whose value may flow (inter-procedurally) to `re`, where it is interpreted
* as a part of a regular expression.
*/
cached
DataFlow::Node regExpSource(DataFlow::Node re) {
exists(RegExpConfiguration c | c.hasFlow(result, re))
}

View File

@@ -8,8 +8,7 @@ private import codeql.ruby.AST as AST
private import codeql.ruby.CFG
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.RemoteFlowSources
private import codeql.ruby.security.performance.ParseRegExp as RegExp
private import codeql.ruby.security.performance.RegExpTreeView
private import codeql.ruby.Regexp
private import codeql.ruby.security.performance.SuperlinearBackTracking
module PolynomialReDoS {

View File

@@ -1,8 +1,10 @@
private import codeql.ruby.ast.Literal as AST
private import ParseRegExp
private import codeql.NumberUtils
/**
* This module should provide a class hierarchy corresponding to a parse tree of regular expressions.
*/
import codeql.ruby.Regexp
import codeql.Locations
private import codeql.ruby.DataFlow
private import codeql.ruby.ast.Literal as AST
/**
* Holds if `term` is an ecape class representing e.g. `\d`.
@@ -59,776 +61,3 @@ module RegExpFlags {
root.getLiteral().isDotAll()
}
}
/**
* Provides utility predicates related to regular expressions.
*/
module RegExpPatterns {
/**
* Gets a pattern that matches common top-level domain names in lower case.
*/
string getACommonTld() {
// according to ranking by http://google.com/search?q=site:.<<TLD>>
result = "(?:com|org|edu|gov|uk|net|io)(?![a-z0-9])"
}
}
/**
* An element containing a regular expression term, that is, either
* a string literal (parsed as a regular expression)
* or another regular expression term.
*/
class RegExpParent extends TRegExpParent {
string toString() { result = "RegExpParent" }
RegExpTerm getChild(int i) { none() }
final RegExpTerm getAChild() { result = this.getChild(_) }
int getNumChild() { result = count(this.getAChild()) }
/**
* Gets the name of a primary CodeQL class to which this regular
* expression term belongs.
*/
string getAPrimaryQlClass() { result = "RegExpParent" }
/**
* Gets a comma-separated list of the names of the primary CodeQL classes to
* which this regular expression term belongs.
*/
final string getPrimaryQlClasses() { result = concat(this.getAPrimaryQlClass(), ",") }
}
class RegExpLiteral extends TRegExpLiteral, RegExpParent {
RegExp re;
RegExpLiteral() { this = TRegExpLiteral(re) }
override RegExpTerm getChild(int i) { i = 0 and result.getRegExp() = re and result.isRootTerm() }
predicate isDotAll() { re.isDotAll() }
predicate isIgnoreCase() { re.isIgnoreCase() }
string getFlags() { result = re.getFlags() }
override string getAPrimaryQlClass() { result = "RegExpLiteral" }
}
class RegExpTerm extends RegExpParent {
RegExp re;
int start;
int end;
RegExpTerm() {
this = TRegExpAlt(re, start, end)
or
this = TRegExpBackRef(re, start, end)
or
this = TRegExpCharacterClass(re, start, end)
or
this = TRegExpCharacterRange(re, start, end)
or
this = TRegExpNormalChar(re, start, end)
or
this = TRegExpGroup(re, start, end)
or
this = TRegExpQuantifier(re, start, end)
or
this = TRegExpSequence(re, start, end) and
exists(seqChild(re, start, end, 1)) // if a sequence does not have more than one element, it should be treated as that element instead.
or
this = TRegExpSpecialChar(re, start, end)
or
this = TRegExpNamedCharacterProperty(re, start, end)
}
RegExpTerm getRootTerm() {
this.isRootTerm() and result = this
or
result = this.getParent().(RegExpTerm).getRootTerm()
}
predicate isUsedAsRegExp() { any() }
predicate isRootTerm() { start = 0 and end = re.getText().length() }
override RegExpTerm getChild(int i) {
result = this.(RegExpAlt).getChild(i)
or
result = this.(RegExpBackRef).getChild(i)
or
result = this.(RegExpCharacterClass).getChild(i)
or
result = this.(RegExpCharacterRange).getChild(i)
or
result = this.(RegExpNormalChar).getChild(i)
or
result = this.(RegExpGroup).getChild(i)
or
result = this.(RegExpQuantifier).getChild(i)
or
result = this.(RegExpSequence).getChild(i)
or
result = this.(RegExpSpecialChar).getChild(i)
or
result = this.(RegExpNamedCharacterProperty).getChild(i)
}
RegExpParent getParent() { result.getAChild() = this }
RegExp getRegExp() { result = re }
int getStart() { result = start }
int getEnd() { result = end }
override string toString() { result = re.getText().substring(start, end) }
override string getAPrimaryQlClass() { result = "RegExpTerm" }
Location getLocation() { result = re.getLocation() }
pragma[noinline]
private predicate componentHasLocationInfo(
int i, string filepath, int startline, int startcolumn, int endline, int endcolumn
) {
re.getComponent(i)
.getLocation()
.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
}
predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn
) {
exists(int re_start, int re_end |
this.componentHasLocationInfo(0, filepath, startline, re_start, _, _) and
this.componentHasLocationInfo(re.getNumberOfComponents() - 1, filepath, _, _, endline, re_end) and
startcolumn = re_start + start and
endcolumn = re_start + end - 1
)
}
File getFile() { result = this.getLocation().getFile() }
string getRawValue() { result = this.toString() }
RegExpLiteral getLiteral() { result = TRegExpLiteral(re) }
/** Gets the regular expression term that is matched (textually) before this one, if any. */
RegExpTerm getPredecessor() {
exists(RegExpTerm parent | parent = this.getParent() |
result = parent.(RegExpSequence).previousElement(this)
or
not exists(parent.(RegExpSequence).previousElement(this)) and
not parent instanceof RegExpSubPattern and
result = parent.getPredecessor()
)
}
/** Gets the regular expression term that is matched (textually) after this one, if any. */
RegExpTerm getSuccessor() {
exists(RegExpTerm parent | parent = this.getParent() |
result = parent.(RegExpSequence).nextElement(this)
or
not exists(parent.(RegExpSequence).nextElement(this)) and
not parent instanceof RegExpSubPattern and
result = parent.getSuccessor()
)
}
}
newtype TRegExpParent =
TRegExpLiteral(RegExp re) or
TRegExpQuantifier(RegExp re, int start, int end) { re.qualifiedItem(start, end, _, _) } or
TRegExpSequence(RegExp re, int start, int end) { re.sequence(start, end) } or
TRegExpAlt(RegExp re, int start, int end) { re.alternation(start, end) } or
TRegExpCharacterClass(RegExp re, int start, int end) { re.charSet(start, end) } or
TRegExpCharacterRange(RegExp re, int start, int end) { re.charRange(_, start, _, _, end) } or
TRegExpGroup(RegExp re, int start, int end) { re.group(start, end) } or
TRegExpSpecialChar(RegExp re, int start, int end) { re.specialCharacter(start, end, _) } or
TRegExpNormalChar(RegExp re, int start, int end) {
re.normalCharacterSequence(start, end)
or
re.escapedCharacter(start, end) and
not re.specialCharacter(start, end, _)
} or
TRegExpBackRef(RegExp re, int start, int end) { re.backreference(start, end) } or
TRegExpNamedCharacterProperty(RegExp re, int start, int end) {
re.namedCharacterProperty(start, end, _)
}
class RegExpQuantifier extends RegExpTerm, TRegExpQuantifier {
int part_end;
boolean may_repeat_forever;
RegExpQuantifier() {
this = TRegExpQuantifier(re, start, end) and
re.qualifiedPart(start, part_end, end, _, may_repeat_forever)
}
override RegExpTerm getChild(int i) {
i = 0 and
result.getRegExp() = re and
result.getStart() = start and
result.getEnd() = part_end
}
predicate mayRepeatForever() { may_repeat_forever = true }
string getQualifier() { result = re.getText().substring(part_end, end) }
override string getAPrimaryQlClass() { result = "RegExpQuantifier" }
}
class InfiniteRepetitionQuantifier extends RegExpQuantifier {
InfiniteRepetitionQuantifier() { this.mayRepeatForever() }
override string getAPrimaryQlClass() { result = "InfiniteRepetitionQuantifier" }
}
class RegExpStar extends InfiniteRepetitionQuantifier {
RegExpStar() { this.getQualifier().charAt(0) = "*" }
override string getAPrimaryQlClass() { result = "RegExpStar" }
}
class RegExpPlus extends InfiniteRepetitionQuantifier {
RegExpPlus() { this.getQualifier().charAt(0) = "+" }
override string getAPrimaryQlClass() { result = "RegExpPlus" }
}
class RegExpOpt extends RegExpQuantifier {
RegExpOpt() { this.getQualifier().charAt(0) = "?" }
override string getAPrimaryQlClass() { result = "RegExpOpt" }
}
class RegExpRange extends RegExpQuantifier {
string upper;
string lower;
RegExpRange() { re.multiples(part_end, end, lower, upper) }
string getUpper() { result = upper }
string getLower() { result = lower }
/**
* Gets the upper bound of the range, if any.
*
* If there is no upper bound, any number of repetitions is allowed.
* For a term of the form `r{lo}`, both the lower and the upper bound
* are `lo`.
*/
int getUpperBound() { result = this.getUpper().toInt() }
/** Gets the lower bound of the range. */
int getLowerBound() { result = this.getLower().toInt() }
override string getAPrimaryQlClass() { result = "RegExpRange" }
}
class RegExpSequence extends RegExpTerm, TRegExpSequence {
RegExpSequence() {
this = TRegExpSequence(re, start, end) and
exists(seqChild(re, start, end, 1)) // if a sequence does not have more than one element, it should be treated as that element instead.
}
override RegExpTerm getChild(int i) { result = seqChild(re, start, end, i) }
/** Gets the element preceding `element` in this sequence. */
RegExpTerm previousElement(RegExpTerm element) { element = this.nextElement(result) }
/** Gets the element following `element` in this sequence. */
RegExpTerm nextElement(RegExpTerm element) {
exists(int i |
element = this.getChild(i) and
result = this.getChild(i + 1)
)
}
override string getAPrimaryQlClass() { result = "RegExpSequence" }
}
pragma[nomagic]
private int seqChildEnd(RegExp re, int start, int end, int i) {
result = seqChild(re, start, end, i).getEnd()
}
// moved out so we can use it in the charpred
private RegExpTerm seqChild(RegExp re, int start, int end, int i) {
re.sequence(start, end) and
(
i = 0 and
result.getRegExp() = re and
result.getStart() = start and
exists(int itemEnd |
re.item(start, itemEnd) and
result.getEnd() = itemEnd
)
or
i > 0 and
result.getRegExp() = re and
exists(int itemStart | itemStart = seqChildEnd(re, start, end, i - 1) |
result.getStart() = itemStart and
re.item(itemStart, result.getEnd())
)
)
}
class RegExpAlt extends RegExpTerm, TRegExpAlt {
RegExpAlt() { this = TRegExpAlt(re, start, end) }
override RegExpTerm getChild(int i) {
i = 0 and
result.getRegExp() = re and
result.getStart() = start and
exists(int part_end |
re.alternationOption(start, end, start, part_end) and
result.getEnd() = part_end
)
or
i > 0 and
result.getRegExp() = re and
exists(int part_start |
part_start = this.getChild(i - 1).getEnd() + 1 // allow for the |
|
result.getStart() = part_start and
re.alternationOption(start, end, part_start, result.getEnd())
)
}
override string getAPrimaryQlClass() { result = "RegExpAlt" }
}
class RegExpCharEscape = RegExpEscape;
class RegExpEscape extends RegExpNormalChar {
RegExpEscape() { re.escapedCharacter(start, end) }
/**
* Gets the name of the escaped; for example, `w` for `\w`.
* TODO: Handle named escapes.
*/
override string getValue() {
this.isIdentityEscape() and result = this.getUnescaped()
or
this.getUnescaped() = "n" and result = "\n"
or
this.getUnescaped() = "r" and result = "\r"
or
this.getUnescaped() = "t" and result = "\t"
or
this.isUnicode() and
result = this.getUnicode()
}
predicate isIdentityEscape() {
not this.getUnescaped() in ["n", "r", "t"] and not this.isUnicode()
}
/**
* Gets the text for this escape. That is e.g. "\w".
*/
private string getText() { result = re.getText().substring(start, end) }
/**
* Holds if this is a unicode escape.
*/
private predicate isUnicode() { this.getText().prefix(2) = ["\\u", "\\U"] }
/**
* Gets the unicode char for this escape.
* E.g. for `\u0061` this returns "a".
*/
private string getUnicode() {
this.isUnicode() and
result = parseHexInt(this.getText().suffix(2)).toUnicode()
}
string getUnescaped() { result = this.getText().suffix(1) }
override string getAPrimaryQlClass() { result = "RegExpEscape" }
}
/**
* A word boundary, that is, a regular expression term of the form `\b`.
*/
class RegExpWordBoundary extends RegExpSpecialChar {
RegExpWordBoundary() { this.getChar() = "\\b" }
}
/**
* A character class escape in a regular expression.
* That is, an escaped character that denotes multiple characters.
*
* Examples:
*
* ```
* \w
* \S
* ```
*/
class RegExpCharacterClassEscape extends RegExpEscape {
RegExpCharacterClassEscape() { this.getValue() in ["d", "D", "s", "S", "w", "W", "h", "H"] }
/** Gets the name of the character class; for example, `w` for `\w`. */
// override string getValue() { result = value }
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpCharacterClassEscape" }
}
/**
* A character class.
*
* Examples:
*
* ```rb
* /[a-fA-F0-9]/
* /[^abc]/
* ```
*/
class RegExpCharacterClass extends RegExpTerm, TRegExpCharacterClass {
RegExpCharacterClass() { this = TRegExpCharacterClass(re, start, end) }
predicate isInverted() { re.getChar(start + 1) = "^" }
predicate isUniversalClass() {
// [^]
this.isInverted() and not exists(this.getAChild())
or
// [\w\W] and similar
not this.isInverted() and
exists(string cce1, string cce2 |
cce1 = this.getAChild().(RegExpCharacterClassEscape).getValue() and
cce2 = this.getAChild().(RegExpCharacterClassEscape).getValue()
|
cce1 != cce2 and cce1.toLowerCase() = cce2.toLowerCase()
)
}
override RegExpTerm getChild(int i) {
i = 0 and
result.getRegExp() = re and
exists(int itemStart, int itemEnd |
result.getStart() = itemStart and
re.charSetStart(start, itemStart) and
re.charSetChild(start, itemStart, itemEnd) and
result.getEnd() = itemEnd
)
or
i > 0 and
result.getRegExp() = re and
exists(int itemStart | itemStart = this.getChild(i - 1).getEnd() |
result.getStart() = itemStart and
re.charSetChild(start, itemStart, result.getEnd())
)
}
override string getAPrimaryQlClass() { result = "RegExpCharacterClass" }
}
class RegExpCharacterRange extends RegExpTerm, TRegExpCharacterRange {
int lower_end;
int upper_start;
RegExpCharacterRange() {
this = TRegExpCharacterRange(re, start, end) and
re.charRange(_, start, lower_end, upper_start, end)
}
predicate isRange(string lo, string hi) {
lo = re.getText().substring(start, lower_end) and
hi = re.getText().substring(upper_start, end)
}
override RegExpTerm getChild(int i) {
i = 0 and
result.getRegExp() = re and
result.getStart() = start and
result.getEnd() = lower_end
or
i = 1 and
result.getRegExp() = re and
result.getStart() = upper_start and
result.getEnd() = end
}
override string getAPrimaryQlClass() { result = "RegExpCharacterRange" }
}
class RegExpNormalChar extends RegExpTerm, TRegExpNormalChar {
RegExpNormalChar() { this = TRegExpNormalChar(re, start, end) }
predicate isCharacter() { any() }
string getValue() { result = re.getText().substring(start, end) }
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpNormalChar" }
}
class RegExpConstant extends RegExpTerm {
string value;
RegExpConstant() {
this = TRegExpNormalChar(re, start, end) and
not this instanceof RegExpCharacterClassEscape and
// exclude chars in qualifiers
// TODO: push this into regex library
not exists(int qstart, int qend | re.qualifiedPart(_, qstart, qend, _, _) |
qstart <= start and end <= qend
) and
value = this.(RegExpNormalChar).getValue()
or
this = TRegExpSpecialChar(re, start, end) and
re.inCharSet(start) and
value = this.(RegExpSpecialChar).getChar()
}
predicate isCharacter() { any() }
string getValue() { result = value }
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpConstant" }
}
class RegExpGroup extends RegExpTerm, TRegExpGroup {
RegExpGroup() { this = TRegExpGroup(re, start, end) }
/**
* Gets the index of this capture group within the enclosing regular
* expression literal.
*
* For example, in the regular expression `/((a?).)(?:b)/`, the
* group `((a?).)` has index 1, the group `(a?)` nested inside it
* has index 2, and the group `(?:b)` has no index, since it is
* not a capture group.
*/
int getNumber() { result = re.getGroupNumber(start, end) }
/** Holds if this is a capture group. */
predicate isCapture() { exists(this.getNumber()) }
/** Holds if this is a named capture group. */
predicate isNamed() { exists(this.getName()) }
/** Gets the name of this capture group, if any. */
string getName() { result = re.getGroupName(start, end) }
predicate isCharacter() { any() }
string getValue() { result = re.getText().substring(start, end) }
override RegExpTerm getChild(int i) {
result.getRegExp() = re and
i = 0 and
re.groupContents(start, end, result.getStart(), result.getEnd())
}
override string getAPrimaryQlClass() { result = "RegExpGroup" }
}
class RegExpSpecialChar extends RegExpTerm, TRegExpSpecialChar {
string char;
RegExpSpecialChar() {
this = TRegExpSpecialChar(re, start, end) and
re.specialCharacter(start, end, char)
}
predicate isCharacter() { any() }
string getChar() { result = char }
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpSpecialChar" }
}
class RegExpDot extends RegExpSpecialChar {
RegExpDot() { this.getChar() = "." }
override string getAPrimaryQlClass() { result = "RegExpDot" }
}
class RegExpDollar extends RegExpSpecialChar {
RegExpDollar() { this.getChar() = ["$", "\\Z", "\\z"] }
override string getAPrimaryQlClass() { result = "RegExpDollar" }
}
class RegExpCaret extends RegExpSpecialChar {
RegExpCaret() { this.getChar() = ["^", "\\A"] }
override string getAPrimaryQlClass() { result = "RegExpCaret" }
}
class RegExpZeroWidthMatch extends RegExpGroup {
RegExpZeroWidthMatch() { re.zeroWidthMatch(start, end) }
override predicate isCharacter() { any() }
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpZeroWidthMatch" }
}
/**
* A zero-width lookahead or lookbehind assertion.
*
* Examples:
*
* ```
* (?=\w)
* (?!\n)
* (?<=\.)
* (?<!\\)
* ```
*/
class RegExpSubPattern extends RegExpZeroWidthMatch {
RegExpSubPattern() { not re.emptyGroup(start, end) }
/** Gets the lookahead term. */
RegExpTerm getOperand() {
exists(int in_start, int in_end | re.groupContents(start, end, in_start, in_end) |
result.getRegExp() = re and
result.getStart() = in_start and
result.getEnd() = in_end
)
}
}
abstract class RegExpLookahead extends RegExpSubPattern { }
class RegExpPositiveLookahead extends RegExpLookahead {
RegExpPositiveLookahead() { re.positiveLookaheadAssertionGroup(start, end) }
override string getAPrimaryQlClass() { result = "RegExpPositiveLookahead" }
}
class RegExpNegativeLookahead extends RegExpLookahead {
RegExpNegativeLookahead() { re.negativeLookaheadAssertionGroup(start, end) }
override string getAPrimaryQlClass() { result = "RegExpNegativeLookahead" }
}
abstract class RegExpLookbehind extends RegExpSubPattern { }
class RegExpPositiveLookbehind extends RegExpLookbehind {
RegExpPositiveLookbehind() { re.positiveLookbehindAssertionGroup(start, end) }
override string getAPrimaryQlClass() { result = "RegExpPositiveLookbehind" }
}
class RegExpNegativeLookbehind extends RegExpLookbehind {
RegExpNegativeLookbehind() { re.negativeLookbehindAssertionGroup(start, end) }
override string getAPrimaryQlClass() { result = "RegExpNegativeLookbehind" }
}
class RegExpBackRef extends RegExpTerm, TRegExpBackRef {
RegExpBackRef() { this = TRegExpBackRef(re, start, end) }
/**
* Gets the number of the capture group this back reference refers to, if any.
*/
int getNumber() { result = re.getBackRefNumber(start, end) }
/**
* Gets the name of the capture group this back reference refers to, if any.
*/
string getName() { result = re.getBackRefName(start, end) }
/** Gets the capture group this back reference refers to. */
RegExpGroup getGroup() {
result.getLiteral() = this.getLiteral() and
(
result.getNumber() = this.getNumber() or
result.getName() = this.getName()
)
}
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpBackRef" }
}
/**
* A named character property. For example, the POSIX bracket expression
* `[[:digit:]]`.
*/
class RegExpNamedCharacterProperty extends RegExpTerm, TRegExpNamedCharacterProperty {
RegExpNamedCharacterProperty() { this = TRegExpNamedCharacterProperty(re, start, end) }
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpNamedCharacterProperty" }
/**
* Gets the property name. For example, in `\p{Space}`, the result is
* `"Space"`.
*/
string getName() { result = re.getCharacterPropertyName(start, end) }
/**
* Holds if the property is inverted. For example, it holds for `\p{^Digit}`,
* which matches non-digits.
*/
predicate isInverted() { re.namedCharacterPropertyIsInverted(start, end) }
}
RegExpTerm getParsedRegExp(AST::RegExpLiteral re) {
result.getRegExp() = re and result.isRootTerm()
}
/**
* A node whose value may flow to a position where it is interpreted
* as a part of a regular expression.
*/
abstract class RegExpPatternSource extends DataFlow::Node {
/**
* Gets a node where the pattern of this node is parsed as a part of
* a regular expression.
*/
abstract DataFlow::Node getAParse();
/**
* Gets the root term of the regular expression parsed from this pattern.
*/
abstract RegExpTerm getRegExpTerm();
}
/**
* A regular expression literal, viewed as the pattern source for itself.
*/
private class RegExpLiteralPatternSource extends RegExpPatternSource {
private AST::RegExpLiteral astNode;
RegExpLiteralPatternSource() { astNode = this.asExpr().getExpr() }
override DataFlow::Node getAParse() { result = this }
override RegExpTerm getRegExpTerm() { result = astNode.getParsed() }
}
/**
* A node whose string value may flow to a position where it is interpreted
* as a part of a regular expression.
*/
private class StringRegExpPatternSource extends RegExpPatternSource {
private DataFlow::Node parse;
StringRegExpPatternSource() { this = regExpSource(parse) }
override DataFlow::Node getAParse() { result = parse }
override RegExpTerm getRegExpTerm() { result.getRegExp() = this.asExpr().getExpr() }
}

View File

@@ -1,2 +1,2 @@
import codeql.ruby.security.performance.RegExpTreeView
import codeql.ruby.Regexp
import codeql.ruby.DataFlow

View File

@@ -16,7 +16,7 @@
import codeql.ruby.security.performance.ExponentialBackTracking
import codeql.ruby.security.performance.ReDoSUtil
import codeql.ruby.security.performance.RegExpTreeView
import codeql.ruby.Regexp
from RegExpTerm t, string pump, State s, string prefixMsg
where hasReDoSResult(t, pump, s, prefixMsg)

View File

@@ -3,9 +3,9 @@
*/
import codeql.Locations
import codeql.ruby.security.performance.RegExpTreeView as RETV
import codeql.ruby.Regexp as RE
query predicate nodes(RETV::RegExpTerm n, string attr, string val) {
query predicate nodes(RE::RegExpTerm n, string attr, string val) {
attr = "semmle.label" and
val = "[" + concat(n.getAPrimaryQlClass(), ", ") + "] " + n.toString()
or
@@ -13,7 +13,7 @@ query predicate nodes(RETV::RegExpTerm n, string attr, string val) {
val =
any(int i |
n =
rank[i](RETV::RegExpTerm t, string fp, int sl, int sc, int el, int ec |
rank[i](RE::RegExpTerm t, string fp, int sl, int sc, int el, int ec |
t.hasLocationInfo(fp, sl, sc, el, ec)
|
t order by fp, sl, sc, el, ec, t.toString()
@@ -21,7 +21,7 @@ query predicate nodes(RETV::RegExpTerm n, string attr, string val) {
).toString()
}
query predicate edges(RETV::RegExpTerm pred, RETV::RegExpTerm succ, string attr, string val) {
query predicate edges(RE::RegExpTerm pred, RE::RegExpTerm succ, string attr, string val) {
attr in ["semmle.label", "semmle.order"] and
val = any(int i | succ = pred.getChild(i)).toString()
}

View File

@@ -1,4 +1,4 @@
import codeql.ruby.security.performance.RegExpTreeView
import codeql.ruby.Regexp
query predicate groupName(RegExpGroup g, string name) { name = g.getName() }

View File

@@ -33,7 +33,9 @@
| tst.rb:137:11:137:17 | (\\w\|G)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'G'. |
| tst.rb:143:11:143:18 | (\\d\|\\w)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '0'. |
| tst.rb:146:11:146:17 | (\\d\|5)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '5'. |
| tst.rb:155:11:155:20 | (\\f\|[\\f])* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'f'. |
| tst.rb:149:11:149:20 | (\\s\|[\\f])* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '\u000c'. |
| tst.rb:152:11:152:24 | (\\s\|[\\v]\|\\\\v)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '\u000b'. |
| tst.rb:155:11:155:20 | (\\f\|[\\f])* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '\u000c'. |
| tst.rb:158:11:158:18 | (\\W\|\\D)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of ' '. |
| tst.rb:161:11:161:18 | (\\S\|\\w)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '0'. |
| tst.rb:164:11:164:20 | (\\S\|[\\w])* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '0'. |