Python: Enrich the NoSql concept

This allows us to make more precise modelling
The query tests now pass.
I do wonder, if there is a cleaner approach, similar to
`TaintedObject` in JavaScript. I want the option to
get this query in the hands of the custumors before
such an investigation, though.
This commit is contained in:
Rasmus Lerchedahl Petersen
2023-08-31 20:30:03 +02:00
parent 114984bd8c
commit c0b3245a53
5 changed files with 80 additions and 12 deletions

View File

@@ -389,6 +389,12 @@ module NoSqlQuery {
abstract class Range extends DataFlow::Node {
/** Gets the argument that specifies the NoSql query to be executed. */
abstract DataFlow::Node getQuery();
/** Holds if this query will unpack/interpret a dictionary */
abstract predicate interpretsDict();
/** Holds if this query can be dangerous when run on a user-controlled string */
abstract predicate vulnerableToStrings();
}
}
@@ -401,6 +407,12 @@ module NoSqlQuery {
class NoSqlQuery extends DataFlow::Node instanceof NoSqlQuery::Range {
/** Gets the argument that specifies the NoSql query to be executed. */
DataFlow::Node getQuery() { result = super.getQuery() }
/** Holds if this query will unpack/interpret a dictionary */
predicate interpretsDict() { super.interpretsDict() }
/** Holds if this query can be dangerous when run on a user-controlled string */
predicate vulnerableToStrings() { super.vulnerableToStrings() }
}
/** Provides classes for modeling NoSql sanitization-related APIs. */

View File

@@ -91,16 +91,17 @@ private module NoSql {
result = mongoDBInstance().getMember(["get_collection", "create_collection"]).getReturn()
}
/** This class represents names of find_* relevant `Mongo` collection-level operation methods. */
private class MongoCollectionMethodNames extends string {
MongoCollectionMethodNames() {
this in [
"find", "find_raw_batches", "find_one", "find_one_and_delete", "find_and_modify",
"find_one_and_replace", "find_one_and_update", "find_one_or_404"
]
}
/** Gets the name of a find_* relevant `Mongo` collection-level operation method. */
private string mongoCollectionMethodName() {
result in [
"find", "find_raw_batches", "find_one", "find_one_and_delete", "find_and_modify",
"find_one_and_replace", "find_one_and_update", "find_one_or_404"
]
}
/** Gets the name of a mongo query operator that will interpret JavaScript. */
private string mongoQueryOperator() { result in ["$where", "$function"] }
/**
* Gets a reference to a `Mongo` collection method call
*
@@ -114,10 +115,29 @@ private module NoSql {
*/
private class MongoCollectionCall extends DataFlow::CallCfgNode, NoSqlQuery::Range {
MongoCollectionCall() {
this = mongoCollection().getMember(any(MongoCollectionMethodNames m)).getACall()
this = mongoCollection().getMember(mongoCollectionMethodName()).getACall()
}
override DataFlow::Node getQuery() { result = this.getArg(0) }
override predicate interpretsDict() { any() }
override predicate vulnerableToStrings() { none() }
}
private class MongoCollectionQueryOperator extends API::CallNode, NoSqlQuery::Range {
DataFlow::Node query;
MongoCollectionQueryOperator() {
this = mongoCollection().getMember(mongoCollectionMethodName()).getACall() and
query = this.getParameter(0).getSubscript(mongoQueryOperator()).asSink()
}
override DataFlow::Node getQuery() { result = query }
override predicate interpretsDict() { none() }
override predicate vulnerableToStrings() { any() }
}
/**
@@ -146,6 +166,10 @@ private module NoSql {
}
override DataFlow::Node getQuery() { result = this.getArgByName(_) }
override predicate interpretsDict() { any() }
override predicate vulnerableToStrings() { none() }
}
/** Gets a reference to `mongosanitizer.sanitizer.sanitize` */
@@ -176,4 +200,23 @@ private module NoSql {
override DataFlow::Node getAnInput() { result = this.getArg(0) }
}
/**
* An equality operator can protect against dictionary interpretation.
* For instance, in `{'password': {"$eq": password} }`, if a dictionary is injected into
* `password`, it will not match.
*/
private class EqualityOperator extends DataFlow::Node, NoSqlSanitizer::Range {
EqualityOperator() {
this =
mongoCollection()
.getMember(mongoCollectionMethodName())
.getParameter(0)
.getASubscript*()
.getSubscript("$eq")
.asSink()
}
override DataFlow::Node getAnInput() { result = this }
}
}

View File

@@ -36,6 +36,14 @@ module NoSqlInjection {
class RemoteFlowSourceAsStringSource extends RemoteFlowSource, StringSource { }
class NoSqlQueryAsStringSink extends StringSink {
NoSqlQueryAsStringSink() {
exists(NoSqlQuery noSqlQuery | this = noSqlQuery.getQuery() |
noSqlQuery.vulnerableToStrings()
)
}
}
class NoSqlQueryAsDictSink extends DictSink {
NoSqlQueryAsDictSink() { this = any(NoSqlQuery noSqlQuery).getQuery() }
}

View File

@@ -17,7 +17,12 @@ module Config implements DataFlow::StateConfigSig {
predicate isSink(DataFlow::Node source, FlowState state) {
source instanceof C::StringSink and
state instanceof C::StringInput
(
state instanceof C::StringInput
or
// since dictionaries can encode strings
state instanceof C::DictInput
)
or
source instanceof C::DictSink and
state instanceof C::DictInput