mirror of
https://github.com/github/codeql.git
synced 2025-12-22 19:56:32 +01:00
Merge branch 'jty/python/emailInjection' of https://github.com/jty-team/codeql into jty/python/emailInjection
This commit is contained in:
3
python/change-notes/2021-09-29-model-asyncpg.md
Normal file
3
python/change-notes/2021-09-29-model-asyncpg.md
Normal file
@@ -0,0 +1,3 @@
|
||||
lgtm,codescanning
|
||||
* Added modeling of `asyncpg` for sinks executing SQL and/or accessing the file system.
|
||||
* Corrected the API graph, such that all awaited values now are referred to via `getAwaited`.
|
||||
2
python/change-notes/2021-10-11-model-aiomysql.md
Normal file
2
python/change-notes/2021-10-11-model-aiomysql.md
Normal file
@@ -0,0 +1,2 @@
|
||||
lgtm,codescanning
|
||||
* Added modeling of `aiomysql` for sinks executing SQL
|
||||
2
python/change-notes/2021-10-25-add-FastAPI-modeling.md
Normal file
2
python/change-notes/2021-10-25-add-FastAPI-modeling.md
Normal file
@@ -0,0 +1,2 @@
|
||||
lgtm,codescanning
|
||||
* Added modeling of sources/sinks when using FastAPI to create web servers.
|
||||
2
python/change-notes/2021-10-28-flask-send_file.md
Normal file
2
python/change-notes/2021-10-28-flask-send_file.md
Normal file
@@ -0,0 +1,2 @@
|
||||
lgtm,codescanning
|
||||
* Added modeling of the `send_from_directory` and `send_file` functions from the `flask` PyPI package, resulting in additional sinks for the _Uncontrolled data used in path expression_ (`py/path-injection`) query. This addition was originally [submitted as an external contribution by @porcupineyhairs](https://github.com/github/codeql/pull/6330).
|
||||
@@ -0,0 +1,2 @@
|
||||
lgtm,codescanning
|
||||
* Added modeling of HTTP requests and responses when using the Django REST Framework (`djangorestframework` PyPI package), which leads to additional remote flow sources.
|
||||
2
python/change-notes/2021-11-02-flask_admin.md
Normal file
2
python/change-notes/2021-11-02-flask_admin.md
Normal file
@@ -0,0 +1,2 @@
|
||||
lgtm,codescanning
|
||||
* Added modeling of HTTP requests and responses when using `flask_admin` (`Flask-Admin` PyPI package), which leads to additional remote flow sources.
|
||||
2
python/change-notes/2021-11-02-toml.md
Normal file
2
python/change-notes/2021-11-02-toml.md
Normal file
@@ -0,0 +1,2 @@
|
||||
lgtm,codescanning
|
||||
* Added modeling of the PyPI package `toml`, which provides encoding/decoding of TOML documents, leading to new taint-tracking steps.
|
||||
2
python/change-notes/2021-11-09-model-aiopg.md
Normal file
2
python/change-notes/2021-11-09-model-aiopg.md
Normal file
@@ -0,0 +1,2 @@
|
||||
lgtm,codescanning
|
||||
* Added modeling of `aiopg` for sinks executing SQL.
|
||||
@@ -304,6 +304,8 @@ module API {
|
||||
* API graph node for the prefix `foo`), in accordance with the usual semantics of Python.
|
||||
*/
|
||||
|
||||
private import semmle.python.internal.Awaited
|
||||
|
||||
cached
|
||||
newtype TApiNode =
|
||||
/** The root of the API graph. */
|
||||
@@ -389,7 +391,7 @@ module API {
|
||||
or
|
||||
// Python 3 only
|
||||
result in [
|
||||
"ascii", "breakpoint", "bytes", "exec",
|
||||
"ascii", "breakpoint", "bytes", "exec", "aiter", "anext",
|
||||
// Exceptions
|
||||
"BlockingIOError", "BrokenPipeError", "ChildProcessError", "ConnectionAbortedError",
|
||||
"ConnectionError", "ConnectionRefusedError", "ConnectionResetError", "FileExistsError",
|
||||
@@ -518,10 +520,9 @@ module API {
|
||||
)
|
||||
or
|
||||
// awaiting
|
||||
exists(Await await, DataFlow::Node awaitedValue |
|
||||
exists(DataFlow::Node awaitedValue |
|
||||
lbl = Label::await() and
|
||||
ref.asExpr() = await and
|
||||
await.getValue() = awaitedValue.asExpr() and
|
||||
ref = awaited(awaitedValue) and
|
||||
pred.flowsTo(awaitedValue)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -326,9 +326,47 @@ module CodeExecution {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that constructs an SQL statement.
|
||||
* Often, it is worthy of an alert if an SQL statement is constructed such that
|
||||
* executing it would be a security risk.
|
||||
*
|
||||
* If it is important that the SQL statement is indeed executed, then use `SQLExecution`.
|
||||
*
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `SqlConstruction::Range` instead.
|
||||
*/
|
||||
class SqlConstruction extends DataFlow::Node {
|
||||
SqlConstruction::Range range;
|
||||
|
||||
SqlConstruction() { this = range }
|
||||
|
||||
/** Gets the argument that specifies the SQL statements to be constructed. */
|
||||
DataFlow::Node getSql() { result = range.getSql() }
|
||||
}
|
||||
|
||||
/** Provides a class for modeling new SQL execution APIs. */
|
||||
module SqlConstruction {
|
||||
/**
|
||||
* A data-flow node that constructs an SQL statement.
|
||||
* Often, it is worthy of an alert if an SQL statement is constructed such that
|
||||
* executing it would be a security risk.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `SqlExecution` instead.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node {
|
||||
/** Gets the argument that specifies the SQL statements to be constructed. */
|
||||
abstract DataFlow::Node getSql();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that executes SQL statements.
|
||||
*
|
||||
* If the context of interest is such that merely constructing an SQL statement
|
||||
* would be valuabe to report, then consider using `SqlConstruction`.
|
||||
*
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `SqlExecution::Range` instead.
|
||||
*/
|
||||
@@ -346,6 +384,9 @@ module SqlExecution {
|
||||
/**
|
||||
* A data-flow node that executes SQL statements.
|
||||
*
|
||||
* If the context of interest is such that merely constructing an SQL statement
|
||||
* would be valuabe to report, then consider using `SqlConstruction`.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `SqlExecution` instead.
|
||||
*/
|
||||
|
||||
@@ -6,13 +6,18 @@
|
||||
// `docs/codeql/support/reusables/frameworks.rst`
|
||||
private import semmle.python.frameworks.Aioch
|
||||
private import semmle.python.frameworks.Aiohttp
|
||||
private import semmle.python.frameworks.Aiomysql
|
||||
private import semmle.python.frameworks.Aiopg
|
||||
private import semmle.python.frameworks.Asyncpg
|
||||
private import semmle.python.frameworks.ClickhouseDriver
|
||||
private import semmle.python.frameworks.Cryptodome
|
||||
private import semmle.python.frameworks.Cryptography
|
||||
private import semmle.python.frameworks.Dill
|
||||
private import semmle.python.frameworks.Django
|
||||
private import semmle.python.frameworks.Fabric
|
||||
private import semmle.python.frameworks.FastApi
|
||||
private import semmle.python.frameworks.Flask
|
||||
private import semmle.python.frameworks.FlaskAdmin
|
||||
private import semmle.python.frameworks.FlaskSqlAlchemy
|
||||
private import semmle.python.frameworks.Idna
|
||||
private import semmle.python.frameworks.Invoke
|
||||
@@ -23,12 +28,16 @@ private import semmle.python.frameworks.Mysql
|
||||
private import semmle.python.frameworks.MySQLdb
|
||||
private import semmle.python.frameworks.Peewee
|
||||
private import semmle.python.frameworks.Psycopg2
|
||||
private import semmle.python.frameworks.Pydantic
|
||||
private import semmle.python.frameworks.PyMySQL
|
||||
private import semmle.python.frameworks.RestFramework
|
||||
private import semmle.python.frameworks.Rsa
|
||||
private import semmle.python.frameworks.RuamelYaml
|
||||
private import semmle.python.frameworks.Simplejson
|
||||
private import semmle.python.frameworks.SqlAlchemy
|
||||
private import semmle.python.frameworks.Starlette
|
||||
private import semmle.python.frameworks.Stdlib
|
||||
private import semmle.python.frameworks.Toml
|
||||
private import semmle.python.frameworks.Tornado
|
||||
private import semmle.python.frameworks.Twisted
|
||||
private import semmle.python.frameworks.Ujson
|
||||
|
||||
@@ -107,7 +107,7 @@ class SpecialMethodCallNode extends PotentialSpecialMethodCallNode {
|
||||
|
||||
SpecialMethodCallNode() {
|
||||
exists(SpecialMethod::Potential pot |
|
||||
this.(SpecialMethod::Potential) = pot and
|
||||
this = pot and
|
||||
pot.getSelf().pointsTo().getClass().lookup(pot.getSpecialMethodName()) = resolvedSpecialMethod
|
||||
)
|
||||
}
|
||||
|
||||
@@ -94,13 +94,13 @@ class AugAssign extends AugAssign_ {
|
||||
* Gets the target of this augmented assignment statement.
|
||||
* That is, the `a` in `a += b`.
|
||||
*/
|
||||
Expr getTarget() { result = this.getOperation().(BinaryExpr).getLeft() }
|
||||
Expr getTarget() { result = this.getOperation().getLeft() }
|
||||
|
||||
/**
|
||||
* Gets the value of this augmented assignment statement.
|
||||
* That is, the `b` in `a += b`.
|
||||
*/
|
||||
Expr getValue() { result = this.getOperation().(BinaryExpr).getRight() }
|
||||
Expr getValue() { result = this.getOperation().getRight() }
|
||||
|
||||
override Stmt getASubStatement() { none() }
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ private module AlgorithmNames {
|
||||
name = ["ARGON2", "PBKDF2", "BCRYPT", "SCRYPT"]
|
||||
}
|
||||
|
||||
predicate isWeakPasswordHashingAlgorithm(string name) { none() }
|
||||
predicate isWeakPasswordHashingAlgorithm(string name) { name = "EVPKDF" }
|
||||
}
|
||||
|
||||
private import AlgorithmNames
|
||||
@@ -85,11 +85,13 @@ abstract class CryptographicAlgorithm extends TCryptographicAlgorithm {
|
||||
|
||||
/**
|
||||
* Holds if the name of this algorithm matches `name` modulo case,
|
||||
* white space, dashes, and underscores.
|
||||
* white space, dashes, underscores, and anything after a dash in the name
|
||||
* (to ignore modes of operation, such as CBC or ECB).
|
||||
*/
|
||||
bindingset[name]
|
||||
predicate matchesName(string name) {
|
||||
name.toUpperCase().regexpReplaceAll("[-_ ]", "") = getName()
|
||||
[name.toUpperCase(), name.toUpperCase().regexpCapture("^(\\w+)(?:-.*)?$", 1)]
|
||||
.regexpReplaceAll("[-_ ]", "") = getName()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
private import DataFlowImplCommon
|
||||
private import DataFlowImplSpecific::Private
|
||||
import DataFlowImplSpecific::Public
|
||||
import DataFlowImplCommonPublic
|
||||
|
||||
/**
|
||||
* A configuration of interprocedural data flow analysis. This defines
|
||||
@@ -94,6 +95,22 @@ abstract class Configuration extends string {
|
||||
*/
|
||||
int fieldFlowBranchLimit() { result = 2 }
|
||||
|
||||
/**
|
||||
* Gets a data flow configuration feature to add restrictions to the set of
|
||||
* valid flow paths.
|
||||
*
|
||||
* - `FeatureHasSourceCallContext`:
|
||||
* Assume that sources have some existing call context to disallow
|
||||
* conflicting return-flow directly following the source.
|
||||
* - `FeatureHasSinkCallContext`:
|
||||
* Assume that sinks have some existing call context to disallow
|
||||
* conflicting argument-to-parameter flow directly preceding the sink.
|
||||
* - `FeatureEqualSourceSinkCallContext`:
|
||||
* Implies both of the above and additionally ensures that the entire flow
|
||||
* path preserves the call context.
|
||||
*/
|
||||
FlowFeature getAFeature() { none() }
|
||||
|
||||
/**
|
||||
* Holds if data may flow from `source` to `sink` for this configuration.
|
||||
*/
|
||||
@@ -349,7 +366,8 @@ private predicate jumpStep(NodeEx node1, NodeEx node2, Configuration config) {
|
||||
not outBarrier(node1, config) and
|
||||
not inBarrier(node2, config) and
|
||||
not fullBarrier(node1, config) and
|
||||
not fullBarrier(node2, config)
|
||||
not fullBarrier(node2, config) and
|
||||
not config.getAFeature() instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
@@ -365,7 +383,8 @@ private predicate additionalJumpStep(NodeEx node1, NodeEx node2, Configuration c
|
||||
not outBarrier(node1, config) and
|
||||
not inBarrier(node2, config) and
|
||||
not fullBarrier(node1, config) and
|
||||
not fullBarrier(node2, config)
|
||||
not fullBarrier(node2, config) and
|
||||
not config.getAFeature() instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
@@ -401,6 +420,20 @@ private predicate viableParamArgEx(DataFlowCall call, ParamNodeEx p, ArgNodeEx a
|
||||
*/
|
||||
private predicate useFieldFlow(Configuration config) { config.fieldFlowBranchLimit() >= 1 }
|
||||
|
||||
private predicate hasSourceCallCtx(Configuration config) {
|
||||
exists(FlowFeature feature | feature = config.getAFeature() |
|
||||
feature instanceof FeatureHasSourceCallContext or
|
||||
feature instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
private predicate hasSinkCallCtx(Configuration config) {
|
||||
exists(FlowFeature feature | feature = config.getAFeature() |
|
||||
feature instanceof FeatureHasSinkCallContext or
|
||||
feature instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
private module Stage1 {
|
||||
class ApApprox = Unit;
|
||||
|
||||
@@ -421,7 +454,7 @@ private module Stage1 {
|
||||
not fullBarrier(node, config) and
|
||||
(
|
||||
sourceNode(node, config) and
|
||||
cc = false
|
||||
if hasSourceCallCtx(config) then cc = true else cc = false
|
||||
or
|
||||
exists(NodeEx mid |
|
||||
fwdFlow(mid, cc, config) and
|
||||
@@ -551,7 +584,7 @@ private module Stage1 {
|
||||
private predicate revFlow0(NodeEx node, boolean toReturn, Configuration config) {
|
||||
fwdFlow(node, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false
|
||||
if hasSinkCallCtx(config) then toReturn = true else toReturn = false
|
||||
or
|
||||
exists(NodeEx mid |
|
||||
localFlowStep(node, mid, config) and
|
||||
@@ -937,6 +970,8 @@ private module Stage2 {
|
||||
|
||||
Cc ccNone() { result instanceof CallContextAny }
|
||||
|
||||
CcCall ccSomeCall() { result instanceof CallContextSomeCall }
|
||||
|
||||
private class LocalCc = Unit;
|
||||
|
||||
bindingset[call, c, outercc]
|
||||
@@ -1004,7 +1039,7 @@ private module Stage2 {
|
||||
predicate fwdFlow(NodeEx node, Cc cc, ApOption argAp, Ap ap, Configuration config) {
|
||||
flowCand(node, _, config) and
|
||||
sourceNode(node, config) and
|
||||
cc = ccNone() and
|
||||
(if hasSourceCallCtx(config) then cc = ccSomeCall() else cc = ccNone()) and
|
||||
argAp = apNone() and
|
||||
ap = getApNil(node)
|
||||
or
|
||||
@@ -1215,7 +1250,7 @@ private module Stage2 {
|
||||
) {
|
||||
fwdFlow(node, _, _, ap, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false and
|
||||
(if hasSinkCallCtx(config) then toReturn = true else toReturn = false) and
|
||||
returnAp = apNone() and
|
||||
ap instanceof ApNil
|
||||
or
|
||||
@@ -1616,6 +1651,8 @@ private module Stage3 {
|
||||
|
||||
Cc ccNone() { result = false }
|
||||
|
||||
CcCall ccSomeCall() { result = true }
|
||||
|
||||
private class LocalCc = Unit;
|
||||
|
||||
bindingset[call, c, outercc]
|
||||
@@ -1697,7 +1734,7 @@ private module Stage3 {
|
||||
private predicate fwdFlow0(NodeEx node, Cc cc, ApOption argAp, Ap ap, Configuration config) {
|
||||
flowCand(node, _, config) and
|
||||
sourceNode(node, config) and
|
||||
cc = ccNone() and
|
||||
(if hasSourceCallCtx(config) then cc = ccSomeCall() else cc = ccNone()) and
|
||||
argAp = apNone() and
|
||||
ap = getApNil(node)
|
||||
or
|
||||
@@ -1908,7 +1945,7 @@ private module Stage3 {
|
||||
) {
|
||||
fwdFlow(node, _, _, ap, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false and
|
||||
(if hasSinkCallCtx(config) then toReturn = true else toReturn = false) and
|
||||
returnAp = apNone() and
|
||||
ap instanceof ApNil
|
||||
or
|
||||
@@ -2366,6 +2403,8 @@ private module Stage4 {
|
||||
|
||||
Cc ccNone() { result instanceof CallContextAny }
|
||||
|
||||
CcCall ccSomeCall() { result instanceof CallContextSomeCall }
|
||||
|
||||
private class LocalCc = LocalCallContext;
|
||||
|
||||
bindingset[call, c, outercc]
|
||||
@@ -2461,7 +2500,7 @@ private module Stage4 {
|
||||
private predicate fwdFlow0(NodeEx node, Cc cc, ApOption argAp, Ap ap, Configuration config) {
|
||||
flowCand(node, _, config) and
|
||||
sourceNode(node, config) and
|
||||
cc = ccNone() and
|
||||
(if hasSourceCallCtx(config) then cc = ccSomeCall() else cc = ccNone()) and
|
||||
argAp = apNone() and
|
||||
ap = getApNil(node)
|
||||
or
|
||||
@@ -2672,7 +2711,7 @@ private module Stage4 {
|
||||
) {
|
||||
fwdFlow(node, _, _, ap, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false and
|
||||
(if hasSinkCallCtx(config) then toReturn = true else toReturn = false) and
|
||||
returnAp = apNone() and
|
||||
ap instanceof ApNil
|
||||
or
|
||||
@@ -3064,7 +3103,11 @@ private newtype TPathNode =
|
||||
// A PathNode is introduced by a source ...
|
||||
Stage4::revFlow(node, config) and
|
||||
sourceNode(node, config) and
|
||||
cc instanceof CallContextAny and
|
||||
(
|
||||
if hasSourceCallCtx(config)
|
||||
then cc instanceof CallContextSomeCall
|
||||
else cc instanceof CallContextAny
|
||||
) and
|
||||
sc instanceof SummaryCtxNone and
|
||||
ap = TAccessPathNil(node.getDataFlowType())
|
||||
or
|
||||
@@ -3076,17 +3119,10 @@ private newtype TPathNode =
|
||||
)
|
||||
} or
|
||||
TPathNodeSink(NodeEx node, Configuration config) {
|
||||
sinkNode(node, pragma[only_bind_into](config)) and
|
||||
Stage4::revFlow(node, pragma[only_bind_into](config)) and
|
||||
(
|
||||
// A sink that is also a source ...
|
||||
sourceNode(node, config)
|
||||
or
|
||||
// ... or a sink that can be reached from a source
|
||||
exists(PathNodeMid mid |
|
||||
pathStep(mid, node, _, _, TAccessPathNil(_)) and
|
||||
pragma[only_bind_into](config) = mid.getConfiguration()
|
||||
)
|
||||
exists(PathNodeMid sink |
|
||||
sink.isAtSink() and
|
||||
node = sink.getNodeEx() and
|
||||
config = sink.getConfiguration()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3403,22 +3439,46 @@ private class PathNodeMid extends PathNodeImpl, TPathNodeMid {
|
||||
// an intermediate step to another intermediate node
|
||||
result = this.getSuccMid()
|
||||
or
|
||||
// a final step to a sink via zero steps means we merge the last two steps to prevent trivial-looking edges
|
||||
exists(PathNodeMid mid, PathNodeSink sink |
|
||||
mid = this.getSuccMid() and
|
||||
mid.getNodeEx() = sink.getNodeEx() and
|
||||
mid.getAp() instanceof AccessPathNil and
|
||||
sink.getConfiguration() = unbindConf(mid.getConfiguration()) and
|
||||
result = sink
|
||||
)
|
||||
// a final step to a sink
|
||||
result = this.getSuccMid().projectToSink()
|
||||
}
|
||||
|
||||
override predicate isSource() {
|
||||
sourceNode(node, config) and
|
||||
cc instanceof CallContextAny and
|
||||
(
|
||||
if hasSourceCallCtx(config)
|
||||
then cc instanceof CallContextSomeCall
|
||||
else cc instanceof CallContextAny
|
||||
) and
|
||||
sc instanceof SummaryCtxNone and
|
||||
ap instanceof AccessPathNil
|
||||
}
|
||||
|
||||
predicate isAtSink() {
|
||||
sinkNode(node, config) and
|
||||
ap instanceof AccessPathNil and
|
||||
if hasSinkCallCtx(config)
|
||||
then
|
||||
// For `FeatureHasSinkCallContext` the condition `cc instanceof CallContextNoCall`
|
||||
// is exactly what we need to check. This also implies
|
||||
// `sc instanceof SummaryCtxNone`.
|
||||
// For `FeatureEqualSourceSinkCallContext` the initial call context was
|
||||
// set to `CallContextSomeCall` and jumps are disallowed, so
|
||||
// `cc instanceof CallContextNoCall` never holds. On the other hand,
|
||||
// in this case there's never any need to enter a call except to identify
|
||||
// a summary, so the condition in `pathIntoCallable` enforces this, which
|
||||
// means that `sc instanceof SummaryCtxNone` holds if and only if we are
|
||||
// in the call context of the source.
|
||||
sc instanceof SummaryCtxNone or
|
||||
cc instanceof CallContextNoCall
|
||||
else any()
|
||||
}
|
||||
|
||||
PathNodeSink projectToSink() {
|
||||
this.isAtSink() and
|
||||
result.getNodeEx() = node and
|
||||
result.getConfiguration() = unbindConf(config)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3572,7 +3632,7 @@ private predicate pathIntoArg(
|
||||
)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
pragma[nomagic]
|
||||
private predicate parameterCand(
|
||||
DataFlowCallable callable, int i, AccessPathApprox apa, Configuration config
|
||||
) {
|
||||
@@ -3613,7 +3673,11 @@ private predicate pathIntoCallable(
|
||||
sc = TSummaryCtxSome(p, ap)
|
||||
or
|
||||
not exists(TSummaryCtxSome(p, ap)) and
|
||||
sc = TSummaryCtxNone()
|
||||
sc = TSummaryCtxNone() and
|
||||
// When the call contexts of source and sink needs to match then there's
|
||||
// never any reason to enter a callable except to find a summary. See also
|
||||
// the comment in `PathNodeMid::isAtSink`.
|
||||
not config.getAFeature() instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
|
|
||||
if recordDataFlowCallSite(call, callable)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
private import DataFlowImplCommon
|
||||
private import DataFlowImplSpecific::Private
|
||||
import DataFlowImplSpecific::Public
|
||||
import DataFlowImplCommonPublic
|
||||
|
||||
/**
|
||||
* A configuration of interprocedural data flow analysis. This defines
|
||||
@@ -94,6 +95,22 @@ abstract class Configuration extends string {
|
||||
*/
|
||||
int fieldFlowBranchLimit() { result = 2 }
|
||||
|
||||
/**
|
||||
* Gets a data flow configuration feature to add restrictions to the set of
|
||||
* valid flow paths.
|
||||
*
|
||||
* - `FeatureHasSourceCallContext`:
|
||||
* Assume that sources have some existing call context to disallow
|
||||
* conflicting return-flow directly following the source.
|
||||
* - `FeatureHasSinkCallContext`:
|
||||
* Assume that sinks have some existing call context to disallow
|
||||
* conflicting argument-to-parameter flow directly preceding the sink.
|
||||
* - `FeatureEqualSourceSinkCallContext`:
|
||||
* Implies both of the above and additionally ensures that the entire flow
|
||||
* path preserves the call context.
|
||||
*/
|
||||
FlowFeature getAFeature() { none() }
|
||||
|
||||
/**
|
||||
* Holds if data may flow from `source` to `sink` for this configuration.
|
||||
*/
|
||||
@@ -349,7 +366,8 @@ private predicate jumpStep(NodeEx node1, NodeEx node2, Configuration config) {
|
||||
not outBarrier(node1, config) and
|
||||
not inBarrier(node2, config) and
|
||||
not fullBarrier(node1, config) and
|
||||
not fullBarrier(node2, config)
|
||||
not fullBarrier(node2, config) and
|
||||
not config.getAFeature() instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
@@ -365,7 +383,8 @@ private predicate additionalJumpStep(NodeEx node1, NodeEx node2, Configuration c
|
||||
not outBarrier(node1, config) and
|
||||
not inBarrier(node2, config) and
|
||||
not fullBarrier(node1, config) and
|
||||
not fullBarrier(node2, config)
|
||||
not fullBarrier(node2, config) and
|
||||
not config.getAFeature() instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
@@ -401,6 +420,20 @@ private predicate viableParamArgEx(DataFlowCall call, ParamNodeEx p, ArgNodeEx a
|
||||
*/
|
||||
private predicate useFieldFlow(Configuration config) { config.fieldFlowBranchLimit() >= 1 }
|
||||
|
||||
private predicate hasSourceCallCtx(Configuration config) {
|
||||
exists(FlowFeature feature | feature = config.getAFeature() |
|
||||
feature instanceof FeatureHasSourceCallContext or
|
||||
feature instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
private predicate hasSinkCallCtx(Configuration config) {
|
||||
exists(FlowFeature feature | feature = config.getAFeature() |
|
||||
feature instanceof FeatureHasSinkCallContext or
|
||||
feature instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
private module Stage1 {
|
||||
class ApApprox = Unit;
|
||||
|
||||
@@ -421,7 +454,7 @@ private module Stage1 {
|
||||
not fullBarrier(node, config) and
|
||||
(
|
||||
sourceNode(node, config) and
|
||||
cc = false
|
||||
if hasSourceCallCtx(config) then cc = true else cc = false
|
||||
or
|
||||
exists(NodeEx mid |
|
||||
fwdFlow(mid, cc, config) and
|
||||
@@ -551,7 +584,7 @@ private module Stage1 {
|
||||
private predicate revFlow0(NodeEx node, boolean toReturn, Configuration config) {
|
||||
fwdFlow(node, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false
|
||||
if hasSinkCallCtx(config) then toReturn = true else toReturn = false
|
||||
or
|
||||
exists(NodeEx mid |
|
||||
localFlowStep(node, mid, config) and
|
||||
@@ -937,6 +970,8 @@ private module Stage2 {
|
||||
|
||||
Cc ccNone() { result instanceof CallContextAny }
|
||||
|
||||
CcCall ccSomeCall() { result instanceof CallContextSomeCall }
|
||||
|
||||
private class LocalCc = Unit;
|
||||
|
||||
bindingset[call, c, outercc]
|
||||
@@ -1004,7 +1039,7 @@ private module Stage2 {
|
||||
predicate fwdFlow(NodeEx node, Cc cc, ApOption argAp, Ap ap, Configuration config) {
|
||||
flowCand(node, _, config) and
|
||||
sourceNode(node, config) and
|
||||
cc = ccNone() and
|
||||
(if hasSourceCallCtx(config) then cc = ccSomeCall() else cc = ccNone()) and
|
||||
argAp = apNone() and
|
||||
ap = getApNil(node)
|
||||
or
|
||||
@@ -1215,7 +1250,7 @@ private module Stage2 {
|
||||
) {
|
||||
fwdFlow(node, _, _, ap, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false and
|
||||
(if hasSinkCallCtx(config) then toReturn = true else toReturn = false) and
|
||||
returnAp = apNone() and
|
||||
ap instanceof ApNil
|
||||
or
|
||||
@@ -1616,6 +1651,8 @@ private module Stage3 {
|
||||
|
||||
Cc ccNone() { result = false }
|
||||
|
||||
CcCall ccSomeCall() { result = true }
|
||||
|
||||
private class LocalCc = Unit;
|
||||
|
||||
bindingset[call, c, outercc]
|
||||
@@ -1697,7 +1734,7 @@ private module Stage3 {
|
||||
private predicate fwdFlow0(NodeEx node, Cc cc, ApOption argAp, Ap ap, Configuration config) {
|
||||
flowCand(node, _, config) and
|
||||
sourceNode(node, config) and
|
||||
cc = ccNone() and
|
||||
(if hasSourceCallCtx(config) then cc = ccSomeCall() else cc = ccNone()) and
|
||||
argAp = apNone() and
|
||||
ap = getApNil(node)
|
||||
or
|
||||
@@ -1908,7 +1945,7 @@ private module Stage3 {
|
||||
) {
|
||||
fwdFlow(node, _, _, ap, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false and
|
||||
(if hasSinkCallCtx(config) then toReturn = true else toReturn = false) and
|
||||
returnAp = apNone() and
|
||||
ap instanceof ApNil
|
||||
or
|
||||
@@ -2366,6 +2403,8 @@ private module Stage4 {
|
||||
|
||||
Cc ccNone() { result instanceof CallContextAny }
|
||||
|
||||
CcCall ccSomeCall() { result instanceof CallContextSomeCall }
|
||||
|
||||
private class LocalCc = LocalCallContext;
|
||||
|
||||
bindingset[call, c, outercc]
|
||||
@@ -2461,7 +2500,7 @@ private module Stage4 {
|
||||
private predicate fwdFlow0(NodeEx node, Cc cc, ApOption argAp, Ap ap, Configuration config) {
|
||||
flowCand(node, _, config) and
|
||||
sourceNode(node, config) and
|
||||
cc = ccNone() and
|
||||
(if hasSourceCallCtx(config) then cc = ccSomeCall() else cc = ccNone()) and
|
||||
argAp = apNone() and
|
||||
ap = getApNil(node)
|
||||
or
|
||||
@@ -2672,7 +2711,7 @@ private module Stage4 {
|
||||
) {
|
||||
fwdFlow(node, _, _, ap, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false and
|
||||
(if hasSinkCallCtx(config) then toReturn = true else toReturn = false) and
|
||||
returnAp = apNone() and
|
||||
ap instanceof ApNil
|
||||
or
|
||||
@@ -3064,7 +3103,11 @@ private newtype TPathNode =
|
||||
// A PathNode is introduced by a source ...
|
||||
Stage4::revFlow(node, config) and
|
||||
sourceNode(node, config) and
|
||||
cc instanceof CallContextAny and
|
||||
(
|
||||
if hasSourceCallCtx(config)
|
||||
then cc instanceof CallContextSomeCall
|
||||
else cc instanceof CallContextAny
|
||||
) and
|
||||
sc instanceof SummaryCtxNone and
|
||||
ap = TAccessPathNil(node.getDataFlowType())
|
||||
or
|
||||
@@ -3076,17 +3119,10 @@ private newtype TPathNode =
|
||||
)
|
||||
} or
|
||||
TPathNodeSink(NodeEx node, Configuration config) {
|
||||
sinkNode(node, pragma[only_bind_into](config)) and
|
||||
Stage4::revFlow(node, pragma[only_bind_into](config)) and
|
||||
(
|
||||
// A sink that is also a source ...
|
||||
sourceNode(node, config)
|
||||
or
|
||||
// ... or a sink that can be reached from a source
|
||||
exists(PathNodeMid mid |
|
||||
pathStep(mid, node, _, _, TAccessPathNil(_)) and
|
||||
pragma[only_bind_into](config) = mid.getConfiguration()
|
||||
)
|
||||
exists(PathNodeMid sink |
|
||||
sink.isAtSink() and
|
||||
node = sink.getNodeEx() and
|
||||
config = sink.getConfiguration()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3403,22 +3439,46 @@ private class PathNodeMid extends PathNodeImpl, TPathNodeMid {
|
||||
// an intermediate step to another intermediate node
|
||||
result = this.getSuccMid()
|
||||
or
|
||||
// a final step to a sink via zero steps means we merge the last two steps to prevent trivial-looking edges
|
||||
exists(PathNodeMid mid, PathNodeSink sink |
|
||||
mid = this.getSuccMid() and
|
||||
mid.getNodeEx() = sink.getNodeEx() and
|
||||
mid.getAp() instanceof AccessPathNil and
|
||||
sink.getConfiguration() = unbindConf(mid.getConfiguration()) and
|
||||
result = sink
|
||||
)
|
||||
// a final step to a sink
|
||||
result = this.getSuccMid().projectToSink()
|
||||
}
|
||||
|
||||
override predicate isSource() {
|
||||
sourceNode(node, config) and
|
||||
cc instanceof CallContextAny and
|
||||
(
|
||||
if hasSourceCallCtx(config)
|
||||
then cc instanceof CallContextSomeCall
|
||||
else cc instanceof CallContextAny
|
||||
) and
|
||||
sc instanceof SummaryCtxNone and
|
||||
ap instanceof AccessPathNil
|
||||
}
|
||||
|
||||
predicate isAtSink() {
|
||||
sinkNode(node, config) and
|
||||
ap instanceof AccessPathNil and
|
||||
if hasSinkCallCtx(config)
|
||||
then
|
||||
// For `FeatureHasSinkCallContext` the condition `cc instanceof CallContextNoCall`
|
||||
// is exactly what we need to check. This also implies
|
||||
// `sc instanceof SummaryCtxNone`.
|
||||
// For `FeatureEqualSourceSinkCallContext` the initial call context was
|
||||
// set to `CallContextSomeCall` and jumps are disallowed, so
|
||||
// `cc instanceof CallContextNoCall` never holds. On the other hand,
|
||||
// in this case there's never any need to enter a call except to identify
|
||||
// a summary, so the condition in `pathIntoCallable` enforces this, which
|
||||
// means that `sc instanceof SummaryCtxNone` holds if and only if we are
|
||||
// in the call context of the source.
|
||||
sc instanceof SummaryCtxNone or
|
||||
cc instanceof CallContextNoCall
|
||||
else any()
|
||||
}
|
||||
|
||||
PathNodeSink projectToSink() {
|
||||
this.isAtSink() and
|
||||
result.getNodeEx() = node and
|
||||
result.getConfiguration() = unbindConf(config)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3572,7 +3632,7 @@ private predicate pathIntoArg(
|
||||
)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
pragma[nomagic]
|
||||
private predicate parameterCand(
|
||||
DataFlowCallable callable, int i, AccessPathApprox apa, Configuration config
|
||||
) {
|
||||
@@ -3613,7 +3673,11 @@ private predicate pathIntoCallable(
|
||||
sc = TSummaryCtxSome(p, ap)
|
||||
or
|
||||
not exists(TSummaryCtxSome(p, ap)) and
|
||||
sc = TSummaryCtxNone()
|
||||
sc = TSummaryCtxNone() and
|
||||
// When the call contexts of source and sink needs to match then there's
|
||||
// never any reason to enter a callable except to find a summary. See also
|
||||
// the comment in `PathNodeMid::isAtSink`.
|
||||
not config.getAFeature() instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
|
|
||||
if recordDataFlowCallSite(call, callable)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
private import DataFlowImplCommon
|
||||
private import DataFlowImplSpecific::Private
|
||||
import DataFlowImplSpecific::Public
|
||||
import DataFlowImplCommonPublic
|
||||
|
||||
/**
|
||||
* A configuration of interprocedural data flow analysis. This defines
|
||||
@@ -94,6 +95,22 @@ abstract class Configuration extends string {
|
||||
*/
|
||||
int fieldFlowBranchLimit() { result = 2 }
|
||||
|
||||
/**
|
||||
* Gets a data flow configuration feature to add restrictions to the set of
|
||||
* valid flow paths.
|
||||
*
|
||||
* - `FeatureHasSourceCallContext`:
|
||||
* Assume that sources have some existing call context to disallow
|
||||
* conflicting return-flow directly following the source.
|
||||
* - `FeatureHasSinkCallContext`:
|
||||
* Assume that sinks have some existing call context to disallow
|
||||
* conflicting argument-to-parameter flow directly preceding the sink.
|
||||
* - `FeatureEqualSourceSinkCallContext`:
|
||||
* Implies both of the above and additionally ensures that the entire flow
|
||||
* path preserves the call context.
|
||||
*/
|
||||
FlowFeature getAFeature() { none() }
|
||||
|
||||
/**
|
||||
* Holds if data may flow from `source` to `sink` for this configuration.
|
||||
*/
|
||||
@@ -349,7 +366,8 @@ private predicate jumpStep(NodeEx node1, NodeEx node2, Configuration config) {
|
||||
not outBarrier(node1, config) and
|
||||
not inBarrier(node2, config) and
|
||||
not fullBarrier(node1, config) and
|
||||
not fullBarrier(node2, config)
|
||||
not fullBarrier(node2, config) and
|
||||
not config.getAFeature() instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
@@ -365,7 +383,8 @@ private predicate additionalJumpStep(NodeEx node1, NodeEx node2, Configuration c
|
||||
not outBarrier(node1, config) and
|
||||
not inBarrier(node2, config) and
|
||||
not fullBarrier(node1, config) and
|
||||
not fullBarrier(node2, config)
|
||||
not fullBarrier(node2, config) and
|
||||
not config.getAFeature() instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
@@ -401,6 +420,20 @@ private predicate viableParamArgEx(DataFlowCall call, ParamNodeEx p, ArgNodeEx a
|
||||
*/
|
||||
private predicate useFieldFlow(Configuration config) { config.fieldFlowBranchLimit() >= 1 }
|
||||
|
||||
private predicate hasSourceCallCtx(Configuration config) {
|
||||
exists(FlowFeature feature | feature = config.getAFeature() |
|
||||
feature instanceof FeatureHasSourceCallContext or
|
||||
feature instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
private predicate hasSinkCallCtx(Configuration config) {
|
||||
exists(FlowFeature feature | feature = config.getAFeature() |
|
||||
feature instanceof FeatureHasSinkCallContext or
|
||||
feature instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
private module Stage1 {
|
||||
class ApApprox = Unit;
|
||||
|
||||
@@ -421,7 +454,7 @@ private module Stage1 {
|
||||
not fullBarrier(node, config) and
|
||||
(
|
||||
sourceNode(node, config) and
|
||||
cc = false
|
||||
if hasSourceCallCtx(config) then cc = true else cc = false
|
||||
or
|
||||
exists(NodeEx mid |
|
||||
fwdFlow(mid, cc, config) and
|
||||
@@ -551,7 +584,7 @@ private module Stage1 {
|
||||
private predicate revFlow0(NodeEx node, boolean toReturn, Configuration config) {
|
||||
fwdFlow(node, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false
|
||||
if hasSinkCallCtx(config) then toReturn = true else toReturn = false
|
||||
or
|
||||
exists(NodeEx mid |
|
||||
localFlowStep(node, mid, config) and
|
||||
@@ -937,6 +970,8 @@ private module Stage2 {
|
||||
|
||||
Cc ccNone() { result instanceof CallContextAny }
|
||||
|
||||
CcCall ccSomeCall() { result instanceof CallContextSomeCall }
|
||||
|
||||
private class LocalCc = Unit;
|
||||
|
||||
bindingset[call, c, outercc]
|
||||
@@ -1004,7 +1039,7 @@ private module Stage2 {
|
||||
predicate fwdFlow(NodeEx node, Cc cc, ApOption argAp, Ap ap, Configuration config) {
|
||||
flowCand(node, _, config) and
|
||||
sourceNode(node, config) and
|
||||
cc = ccNone() and
|
||||
(if hasSourceCallCtx(config) then cc = ccSomeCall() else cc = ccNone()) and
|
||||
argAp = apNone() and
|
||||
ap = getApNil(node)
|
||||
or
|
||||
@@ -1215,7 +1250,7 @@ private module Stage2 {
|
||||
) {
|
||||
fwdFlow(node, _, _, ap, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false and
|
||||
(if hasSinkCallCtx(config) then toReturn = true else toReturn = false) and
|
||||
returnAp = apNone() and
|
||||
ap instanceof ApNil
|
||||
or
|
||||
@@ -1616,6 +1651,8 @@ private module Stage3 {
|
||||
|
||||
Cc ccNone() { result = false }
|
||||
|
||||
CcCall ccSomeCall() { result = true }
|
||||
|
||||
private class LocalCc = Unit;
|
||||
|
||||
bindingset[call, c, outercc]
|
||||
@@ -1697,7 +1734,7 @@ private module Stage3 {
|
||||
private predicate fwdFlow0(NodeEx node, Cc cc, ApOption argAp, Ap ap, Configuration config) {
|
||||
flowCand(node, _, config) and
|
||||
sourceNode(node, config) and
|
||||
cc = ccNone() and
|
||||
(if hasSourceCallCtx(config) then cc = ccSomeCall() else cc = ccNone()) and
|
||||
argAp = apNone() and
|
||||
ap = getApNil(node)
|
||||
or
|
||||
@@ -1908,7 +1945,7 @@ private module Stage3 {
|
||||
) {
|
||||
fwdFlow(node, _, _, ap, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false and
|
||||
(if hasSinkCallCtx(config) then toReturn = true else toReturn = false) and
|
||||
returnAp = apNone() and
|
||||
ap instanceof ApNil
|
||||
or
|
||||
@@ -2366,6 +2403,8 @@ private module Stage4 {
|
||||
|
||||
Cc ccNone() { result instanceof CallContextAny }
|
||||
|
||||
CcCall ccSomeCall() { result instanceof CallContextSomeCall }
|
||||
|
||||
private class LocalCc = LocalCallContext;
|
||||
|
||||
bindingset[call, c, outercc]
|
||||
@@ -2461,7 +2500,7 @@ private module Stage4 {
|
||||
private predicate fwdFlow0(NodeEx node, Cc cc, ApOption argAp, Ap ap, Configuration config) {
|
||||
flowCand(node, _, config) and
|
||||
sourceNode(node, config) and
|
||||
cc = ccNone() and
|
||||
(if hasSourceCallCtx(config) then cc = ccSomeCall() else cc = ccNone()) and
|
||||
argAp = apNone() and
|
||||
ap = getApNil(node)
|
||||
or
|
||||
@@ -2672,7 +2711,7 @@ private module Stage4 {
|
||||
) {
|
||||
fwdFlow(node, _, _, ap, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false and
|
||||
(if hasSinkCallCtx(config) then toReturn = true else toReturn = false) and
|
||||
returnAp = apNone() and
|
||||
ap instanceof ApNil
|
||||
or
|
||||
@@ -3064,7 +3103,11 @@ private newtype TPathNode =
|
||||
// A PathNode is introduced by a source ...
|
||||
Stage4::revFlow(node, config) and
|
||||
sourceNode(node, config) and
|
||||
cc instanceof CallContextAny and
|
||||
(
|
||||
if hasSourceCallCtx(config)
|
||||
then cc instanceof CallContextSomeCall
|
||||
else cc instanceof CallContextAny
|
||||
) and
|
||||
sc instanceof SummaryCtxNone and
|
||||
ap = TAccessPathNil(node.getDataFlowType())
|
||||
or
|
||||
@@ -3076,17 +3119,10 @@ private newtype TPathNode =
|
||||
)
|
||||
} or
|
||||
TPathNodeSink(NodeEx node, Configuration config) {
|
||||
sinkNode(node, pragma[only_bind_into](config)) and
|
||||
Stage4::revFlow(node, pragma[only_bind_into](config)) and
|
||||
(
|
||||
// A sink that is also a source ...
|
||||
sourceNode(node, config)
|
||||
or
|
||||
// ... or a sink that can be reached from a source
|
||||
exists(PathNodeMid mid |
|
||||
pathStep(mid, node, _, _, TAccessPathNil(_)) and
|
||||
pragma[only_bind_into](config) = mid.getConfiguration()
|
||||
)
|
||||
exists(PathNodeMid sink |
|
||||
sink.isAtSink() and
|
||||
node = sink.getNodeEx() and
|
||||
config = sink.getConfiguration()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3403,22 +3439,46 @@ private class PathNodeMid extends PathNodeImpl, TPathNodeMid {
|
||||
// an intermediate step to another intermediate node
|
||||
result = this.getSuccMid()
|
||||
or
|
||||
// a final step to a sink via zero steps means we merge the last two steps to prevent trivial-looking edges
|
||||
exists(PathNodeMid mid, PathNodeSink sink |
|
||||
mid = this.getSuccMid() and
|
||||
mid.getNodeEx() = sink.getNodeEx() and
|
||||
mid.getAp() instanceof AccessPathNil and
|
||||
sink.getConfiguration() = unbindConf(mid.getConfiguration()) and
|
||||
result = sink
|
||||
)
|
||||
// a final step to a sink
|
||||
result = this.getSuccMid().projectToSink()
|
||||
}
|
||||
|
||||
override predicate isSource() {
|
||||
sourceNode(node, config) and
|
||||
cc instanceof CallContextAny and
|
||||
(
|
||||
if hasSourceCallCtx(config)
|
||||
then cc instanceof CallContextSomeCall
|
||||
else cc instanceof CallContextAny
|
||||
) and
|
||||
sc instanceof SummaryCtxNone and
|
||||
ap instanceof AccessPathNil
|
||||
}
|
||||
|
||||
predicate isAtSink() {
|
||||
sinkNode(node, config) and
|
||||
ap instanceof AccessPathNil and
|
||||
if hasSinkCallCtx(config)
|
||||
then
|
||||
// For `FeatureHasSinkCallContext` the condition `cc instanceof CallContextNoCall`
|
||||
// is exactly what we need to check. This also implies
|
||||
// `sc instanceof SummaryCtxNone`.
|
||||
// For `FeatureEqualSourceSinkCallContext` the initial call context was
|
||||
// set to `CallContextSomeCall` and jumps are disallowed, so
|
||||
// `cc instanceof CallContextNoCall` never holds. On the other hand,
|
||||
// in this case there's never any need to enter a call except to identify
|
||||
// a summary, so the condition in `pathIntoCallable` enforces this, which
|
||||
// means that `sc instanceof SummaryCtxNone` holds if and only if we are
|
||||
// in the call context of the source.
|
||||
sc instanceof SummaryCtxNone or
|
||||
cc instanceof CallContextNoCall
|
||||
else any()
|
||||
}
|
||||
|
||||
PathNodeSink projectToSink() {
|
||||
this.isAtSink() and
|
||||
result.getNodeEx() = node and
|
||||
result.getConfiguration() = unbindConf(config)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3572,7 +3632,7 @@ private predicate pathIntoArg(
|
||||
)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
pragma[nomagic]
|
||||
private predicate parameterCand(
|
||||
DataFlowCallable callable, int i, AccessPathApprox apa, Configuration config
|
||||
) {
|
||||
@@ -3613,7 +3673,11 @@ private predicate pathIntoCallable(
|
||||
sc = TSummaryCtxSome(p, ap)
|
||||
or
|
||||
not exists(TSummaryCtxSome(p, ap)) and
|
||||
sc = TSummaryCtxNone()
|
||||
sc = TSummaryCtxNone() and
|
||||
// When the call contexts of source and sink needs to match then there's
|
||||
// never any reason to enter a callable except to find a summary. See also
|
||||
// the comment in `PathNodeMid::isAtSink`.
|
||||
not config.getAFeature() instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
|
|
||||
if recordDataFlowCallSite(call, callable)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
private import DataFlowImplCommon
|
||||
private import DataFlowImplSpecific::Private
|
||||
import DataFlowImplSpecific::Public
|
||||
import DataFlowImplCommonPublic
|
||||
|
||||
/**
|
||||
* A configuration of interprocedural data flow analysis. This defines
|
||||
@@ -94,6 +95,22 @@ abstract class Configuration extends string {
|
||||
*/
|
||||
int fieldFlowBranchLimit() { result = 2 }
|
||||
|
||||
/**
|
||||
* Gets a data flow configuration feature to add restrictions to the set of
|
||||
* valid flow paths.
|
||||
*
|
||||
* - `FeatureHasSourceCallContext`:
|
||||
* Assume that sources have some existing call context to disallow
|
||||
* conflicting return-flow directly following the source.
|
||||
* - `FeatureHasSinkCallContext`:
|
||||
* Assume that sinks have some existing call context to disallow
|
||||
* conflicting argument-to-parameter flow directly preceding the sink.
|
||||
* - `FeatureEqualSourceSinkCallContext`:
|
||||
* Implies both of the above and additionally ensures that the entire flow
|
||||
* path preserves the call context.
|
||||
*/
|
||||
FlowFeature getAFeature() { none() }
|
||||
|
||||
/**
|
||||
* Holds if data may flow from `source` to `sink` for this configuration.
|
||||
*/
|
||||
@@ -349,7 +366,8 @@ private predicate jumpStep(NodeEx node1, NodeEx node2, Configuration config) {
|
||||
not outBarrier(node1, config) and
|
||||
not inBarrier(node2, config) and
|
||||
not fullBarrier(node1, config) and
|
||||
not fullBarrier(node2, config)
|
||||
not fullBarrier(node2, config) and
|
||||
not config.getAFeature() instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
@@ -365,7 +383,8 @@ private predicate additionalJumpStep(NodeEx node1, NodeEx node2, Configuration c
|
||||
not outBarrier(node1, config) and
|
||||
not inBarrier(node2, config) and
|
||||
not fullBarrier(node1, config) and
|
||||
not fullBarrier(node2, config)
|
||||
not fullBarrier(node2, config) and
|
||||
not config.getAFeature() instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
@@ -401,6 +420,20 @@ private predicate viableParamArgEx(DataFlowCall call, ParamNodeEx p, ArgNodeEx a
|
||||
*/
|
||||
private predicate useFieldFlow(Configuration config) { config.fieldFlowBranchLimit() >= 1 }
|
||||
|
||||
private predicate hasSourceCallCtx(Configuration config) {
|
||||
exists(FlowFeature feature | feature = config.getAFeature() |
|
||||
feature instanceof FeatureHasSourceCallContext or
|
||||
feature instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
private predicate hasSinkCallCtx(Configuration config) {
|
||||
exists(FlowFeature feature | feature = config.getAFeature() |
|
||||
feature instanceof FeatureHasSinkCallContext or
|
||||
feature instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
}
|
||||
|
||||
private module Stage1 {
|
||||
class ApApprox = Unit;
|
||||
|
||||
@@ -421,7 +454,7 @@ private module Stage1 {
|
||||
not fullBarrier(node, config) and
|
||||
(
|
||||
sourceNode(node, config) and
|
||||
cc = false
|
||||
if hasSourceCallCtx(config) then cc = true else cc = false
|
||||
or
|
||||
exists(NodeEx mid |
|
||||
fwdFlow(mid, cc, config) and
|
||||
@@ -551,7 +584,7 @@ private module Stage1 {
|
||||
private predicate revFlow0(NodeEx node, boolean toReturn, Configuration config) {
|
||||
fwdFlow(node, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false
|
||||
if hasSinkCallCtx(config) then toReturn = true else toReturn = false
|
||||
or
|
||||
exists(NodeEx mid |
|
||||
localFlowStep(node, mid, config) and
|
||||
@@ -937,6 +970,8 @@ private module Stage2 {
|
||||
|
||||
Cc ccNone() { result instanceof CallContextAny }
|
||||
|
||||
CcCall ccSomeCall() { result instanceof CallContextSomeCall }
|
||||
|
||||
private class LocalCc = Unit;
|
||||
|
||||
bindingset[call, c, outercc]
|
||||
@@ -1004,7 +1039,7 @@ private module Stage2 {
|
||||
predicate fwdFlow(NodeEx node, Cc cc, ApOption argAp, Ap ap, Configuration config) {
|
||||
flowCand(node, _, config) and
|
||||
sourceNode(node, config) and
|
||||
cc = ccNone() and
|
||||
(if hasSourceCallCtx(config) then cc = ccSomeCall() else cc = ccNone()) and
|
||||
argAp = apNone() and
|
||||
ap = getApNil(node)
|
||||
or
|
||||
@@ -1215,7 +1250,7 @@ private module Stage2 {
|
||||
) {
|
||||
fwdFlow(node, _, _, ap, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false and
|
||||
(if hasSinkCallCtx(config) then toReturn = true else toReturn = false) and
|
||||
returnAp = apNone() and
|
||||
ap instanceof ApNil
|
||||
or
|
||||
@@ -1616,6 +1651,8 @@ private module Stage3 {
|
||||
|
||||
Cc ccNone() { result = false }
|
||||
|
||||
CcCall ccSomeCall() { result = true }
|
||||
|
||||
private class LocalCc = Unit;
|
||||
|
||||
bindingset[call, c, outercc]
|
||||
@@ -1697,7 +1734,7 @@ private module Stage3 {
|
||||
private predicate fwdFlow0(NodeEx node, Cc cc, ApOption argAp, Ap ap, Configuration config) {
|
||||
flowCand(node, _, config) and
|
||||
sourceNode(node, config) and
|
||||
cc = ccNone() and
|
||||
(if hasSourceCallCtx(config) then cc = ccSomeCall() else cc = ccNone()) and
|
||||
argAp = apNone() and
|
||||
ap = getApNil(node)
|
||||
or
|
||||
@@ -1908,7 +1945,7 @@ private module Stage3 {
|
||||
) {
|
||||
fwdFlow(node, _, _, ap, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false and
|
||||
(if hasSinkCallCtx(config) then toReturn = true else toReturn = false) and
|
||||
returnAp = apNone() and
|
||||
ap instanceof ApNil
|
||||
or
|
||||
@@ -2366,6 +2403,8 @@ private module Stage4 {
|
||||
|
||||
Cc ccNone() { result instanceof CallContextAny }
|
||||
|
||||
CcCall ccSomeCall() { result instanceof CallContextSomeCall }
|
||||
|
||||
private class LocalCc = LocalCallContext;
|
||||
|
||||
bindingset[call, c, outercc]
|
||||
@@ -2461,7 +2500,7 @@ private module Stage4 {
|
||||
private predicate fwdFlow0(NodeEx node, Cc cc, ApOption argAp, Ap ap, Configuration config) {
|
||||
flowCand(node, _, config) and
|
||||
sourceNode(node, config) and
|
||||
cc = ccNone() and
|
||||
(if hasSourceCallCtx(config) then cc = ccSomeCall() else cc = ccNone()) and
|
||||
argAp = apNone() and
|
||||
ap = getApNil(node)
|
||||
or
|
||||
@@ -2672,7 +2711,7 @@ private module Stage4 {
|
||||
) {
|
||||
fwdFlow(node, _, _, ap, config) and
|
||||
sinkNode(node, config) and
|
||||
toReturn = false and
|
||||
(if hasSinkCallCtx(config) then toReturn = true else toReturn = false) and
|
||||
returnAp = apNone() and
|
||||
ap instanceof ApNil
|
||||
or
|
||||
@@ -3064,7 +3103,11 @@ private newtype TPathNode =
|
||||
// A PathNode is introduced by a source ...
|
||||
Stage4::revFlow(node, config) and
|
||||
sourceNode(node, config) and
|
||||
cc instanceof CallContextAny and
|
||||
(
|
||||
if hasSourceCallCtx(config)
|
||||
then cc instanceof CallContextSomeCall
|
||||
else cc instanceof CallContextAny
|
||||
) and
|
||||
sc instanceof SummaryCtxNone and
|
||||
ap = TAccessPathNil(node.getDataFlowType())
|
||||
or
|
||||
@@ -3076,17 +3119,10 @@ private newtype TPathNode =
|
||||
)
|
||||
} or
|
||||
TPathNodeSink(NodeEx node, Configuration config) {
|
||||
sinkNode(node, pragma[only_bind_into](config)) and
|
||||
Stage4::revFlow(node, pragma[only_bind_into](config)) and
|
||||
(
|
||||
// A sink that is also a source ...
|
||||
sourceNode(node, config)
|
||||
or
|
||||
// ... or a sink that can be reached from a source
|
||||
exists(PathNodeMid mid |
|
||||
pathStep(mid, node, _, _, TAccessPathNil(_)) and
|
||||
pragma[only_bind_into](config) = mid.getConfiguration()
|
||||
)
|
||||
exists(PathNodeMid sink |
|
||||
sink.isAtSink() and
|
||||
node = sink.getNodeEx() and
|
||||
config = sink.getConfiguration()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3403,22 +3439,46 @@ private class PathNodeMid extends PathNodeImpl, TPathNodeMid {
|
||||
// an intermediate step to another intermediate node
|
||||
result = this.getSuccMid()
|
||||
or
|
||||
// a final step to a sink via zero steps means we merge the last two steps to prevent trivial-looking edges
|
||||
exists(PathNodeMid mid, PathNodeSink sink |
|
||||
mid = this.getSuccMid() and
|
||||
mid.getNodeEx() = sink.getNodeEx() and
|
||||
mid.getAp() instanceof AccessPathNil and
|
||||
sink.getConfiguration() = unbindConf(mid.getConfiguration()) and
|
||||
result = sink
|
||||
)
|
||||
// a final step to a sink
|
||||
result = this.getSuccMid().projectToSink()
|
||||
}
|
||||
|
||||
override predicate isSource() {
|
||||
sourceNode(node, config) and
|
||||
cc instanceof CallContextAny and
|
||||
(
|
||||
if hasSourceCallCtx(config)
|
||||
then cc instanceof CallContextSomeCall
|
||||
else cc instanceof CallContextAny
|
||||
) and
|
||||
sc instanceof SummaryCtxNone and
|
||||
ap instanceof AccessPathNil
|
||||
}
|
||||
|
||||
predicate isAtSink() {
|
||||
sinkNode(node, config) and
|
||||
ap instanceof AccessPathNil and
|
||||
if hasSinkCallCtx(config)
|
||||
then
|
||||
// For `FeatureHasSinkCallContext` the condition `cc instanceof CallContextNoCall`
|
||||
// is exactly what we need to check. This also implies
|
||||
// `sc instanceof SummaryCtxNone`.
|
||||
// For `FeatureEqualSourceSinkCallContext` the initial call context was
|
||||
// set to `CallContextSomeCall` and jumps are disallowed, so
|
||||
// `cc instanceof CallContextNoCall` never holds. On the other hand,
|
||||
// in this case there's never any need to enter a call except to identify
|
||||
// a summary, so the condition in `pathIntoCallable` enforces this, which
|
||||
// means that `sc instanceof SummaryCtxNone` holds if and only if we are
|
||||
// in the call context of the source.
|
||||
sc instanceof SummaryCtxNone or
|
||||
cc instanceof CallContextNoCall
|
||||
else any()
|
||||
}
|
||||
|
||||
PathNodeSink projectToSink() {
|
||||
this.isAtSink() and
|
||||
result.getNodeEx() = node and
|
||||
result.getConfiguration() = unbindConf(config)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3572,7 +3632,7 @@ private predicate pathIntoArg(
|
||||
)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
pragma[nomagic]
|
||||
private predicate parameterCand(
|
||||
DataFlowCallable callable, int i, AccessPathApprox apa, Configuration config
|
||||
) {
|
||||
@@ -3613,7 +3673,11 @@ private predicate pathIntoCallable(
|
||||
sc = TSummaryCtxSome(p, ap)
|
||||
or
|
||||
not exists(TSummaryCtxSome(p, ap)) and
|
||||
sc = TSummaryCtxNone()
|
||||
sc = TSummaryCtxNone() and
|
||||
// When the call contexts of source and sink needs to match then there's
|
||||
// never any reason to enter a callable except to find a summary. See also
|
||||
// the comment in `PathNodeMid::isAtSink`.
|
||||
not config.getAFeature() instanceof FeatureEqualSourceSinkCallContext
|
||||
)
|
||||
|
|
||||
if recordDataFlowCallSite(call, callable)
|
||||
|
||||
@@ -2,6 +2,42 @@ private import DataFlowImplSpecific::Private
|
||||
private import DataFlowImplSpecific::Public
|
||||
import Cached
|
||||
|
||||
module DataFlowImplCommonPublic {
|
||||
private newtype TFlowFeature =
|
||||
TFeatureHasSourceCallContext() or
|
||||
TFeatureHasSinkCallContext() or
|
||||
TFeatureEqualSourceSinkCallContext()
|
||||
|
||||
/** A flow configuration feature for use in `Configuration::getAFeature()`. */
|
||||
class FlowFeature extends TFlowFeature {
|
||||
string toString() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A flow configuration feature that implies that sources have some existing
|
||||
* call context.
|
||||
*/
|
||||
class FeatureHasSourceCallContext extends FlowFeature, TFeatureHasSourceCallContext {
|
||||
override string toString() { result = "FeatureHasSourceCallContext" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A flow configuration feature that implies that sinks have some existing
|
||||
* call context.
|
||||
*/
|
||||
class FeatureHasSinkCallContext extends FlowFeature, TFeatureHasSinkCallContext {
|
||||
override string toString() { result = "FeatureHasSinkCallContext" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A flow configuration feature that implies that source-sink pairs have some
|
||||
* shared existing call context.
|
||||
*/
|
||||
class FeatureEqualSourceSinkCallContext extends FlowFeature, TFeatureEqualSourceSinkCallContext {
|
||||
override string toString() { result = "FeatureEqualSourceSinkCallContext" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The cost limits for the `AccessPathFront` to `AccessPathApprox` expansion.
|
||||
*
|
||||
@@ -251,7 +287,7 @@ private module Cached {
|
||||
predicate forceCachingInSameStage() { any() }
|
||||
|
||||
cached
|
||||
predicate nodeEnclosingCallable(Node n, DataFlowCallable c) { c = n.getEnclosingCallable() }
|
||||
predicate nodeEnclosingCallable(Node n, DataFlowCallable c) { c = nodeGetEnclosingCallable(n) }
|
||||
|
||||
cached
|
||||
predicate callEnclosingCallable(DataFlowCall call, DataFlowCallable c) {
|
||||
@@ -316,9 +352,7 @@ private module Cached {
|
||||
}
|
||||
|
||||
cached
|
||||
predicate parameterNode(Node n, DataFlowCallable c, int i) {
|
||||
n.(ParameterNode).isParameterOf(c, i)
|
||||
}
|
||||
predicate parameterNode(Node p, DataFlowCallable c, int pos) { isParameterNode(p, c, pos) }
|
||||
|
||||
cached
|
||||
predicate argumentNode(Node n, DataFlowCall call, int pos) {
|
||||
|
||||
@@ -31,7 +31,7 @@ module Consistency {
|
||||
query predicate uniqueEnclosingCallable(Node n, string msg) {
|
||||
exists(int c |
|
||||
n instanceof RelevantNode and
|
||||
c = count(n.getEnclosingCallable()) and
|
||||
c = count(nodeGetEnclosingCallable(n)) and
|
||||
c != 1 and
|
||||
msg = "Node should have one enclosing callable but has " + c + "."
|
||||
)
|
||||
@@ -85,13 +85,13 @@ module Consistency {
|
||||
}
|
||||
|
||||
query predicate parameterCallable(ParameterNode p, string msg) {
|
||||
exists(DataFlowCallable c | p.isParameterOf(c, _) and c != p.getEnclosingCallable()) and
|
||||
exists(DataFlowCallable c | isParameterNode(p, c, _) and c != nodeGetEnclosingCallable(p)) and
|
||||
msg = "Callable mismatch for parameter."
|
||||
}
|
||||
|
||||
query predicate localFlowIsLocal(Node n1, Node n2, string msg) {
|
||||
simpleLocalFlowStep(n1, n2) and
|
||||
n1.getEnclosingCallable() != n2.getEnclosingCallable() and
|
||||
nodeGetEnclosingCallable(n1) != nodeGetEnclosingCallable(n2) and
|
||||
msg = "Local flow step does not preserve enclosing callable."
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ module Consistency {
|
||||
query predicate unreachableNodeCCtx(Node n, DataFlowCall call, string msg) {
|
||||
isUnreachableInCall(n, call) and
|
||||
exists(DataFlowCallable c |
|
||||
c = n.getEnclosingCallable() and
|
||||
c = nodeGetEnclosingCallable(n) and
|
||||
not viableCallable(call) = c
|
||||
) and
|
||||
msg = "Call context for isUnreachableInCall is inconsistent with call graph."
|
||||
@@ -120,7 +120,7 @@ module Consistency {
|
||||
n.(ArgumentNode).argumentOf(call, _) and
|
||||
msg = "ArgumentNode and call does not share enclosing callable."
|
||||
) and
|
||||
n.getEnclosingCallable() != call.getEnclosingCallable()
|
||||
nodeGetEnclosingCallable(n) != call.getEnclosingCallable()
|
||||
}
|
||||
|
||||
// This predicate helps the compiler forget that in some languages
|
||||
@@ -151,7 +151,7 @@ module Consistency {
|
||||
}
|
||||
|
||||
query predicate postIsInSameCallable(PostUpdateNode n, string msg) {
|
||||
n.getEnclosingCallable() != n.getPreUpdateNode().getEnclosingCallable() and
|
||||
nodeGetEnclosingCallable(n) != nodeGetEnclosingCallable(n.getPreUpdateNode()) and
|
||||
msg = "PostUpdateNode does not share callable with its pre-update node."
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,12 @@ private import DataFlowPublic
|
||||
import semmle.python.SpecialMethods
|
||||
private import semmle.python.essa.SsaCompute
|
||||
|
||||
/** Gets the callable in which this node occurs. */
|
||||
DataFlowCallable nodeGetEnclosingCallable(Node n) { result = n.getEnclosingCallable() }
|
||||
|
||||
/** Holds if `p` is a `ParameterNode` of `c` with position `pos`. */
|
||||
predicate isParameterNode(ParameterNode p, DataFlowCallable c, int pos) { p.isParameterOf(c, pos) }
|
||||
|
||||
//--------
|
||||
// Data flow graph
|
||||
//--------
|
||||
@@ -172,9 +178,23 @@ module EssaFlow {
|
||||
// see `with_flow` in `python/ql/src/semmle/python/dataflow/Implementation.qll`
|
||||
with.getContextExpr() = contextManager.getNode() and
|
||||
with.getOptionalVars() = var.getNode() and
|
||||
not with.isAsync() and
|
||||
contextManager.strictlyDominates(var)
|
||||
)
|
||||
or
|
||||
// Async with var definition
|
||||
// `async with f(42) as x:`
|
||||
// nodeFrom is `x`, cfg node
|
||||
// nodeTo is `x`, essa var
|
||||
//
|
||||
// This makes the cfg node the local source of the awaited value.
|
||||
exists(With with, ControlFlowNode var |
|
||||
nodeFrom.(CfgNode).getNode() = var and
|
||||
nodeTo.(EssaNode).getVar().getDefinition().(WithDefinition).getDefiningNode() = var and
|
||||
with.getOptionalVars() = var.getNode() and
|
||||
with.isAsync()
|
||||
)
|
||||
or
|
||||
// Parameter definition
|
||||
// `def foo(x):`
|
||||
// nodeFrom is `x`, cfgNode
|
||||
@@ -1345,10 +1365,8 @@ module IterableUnpacking {
|
||||
}
|
||||
|
||||
/** A (possibly recursive) target of an unpacking assignment which is also a sequence. */
|
||||
class UnpackingAssignmentSequenceTarget extends UnpackingAssignmentTarget {
|
||||
UnpackingAssignmentSequenceTarget() { this instanceof SequenceNode }
|
||||
|
||||
ControlFlowNode getElement(int i) { result = this.(SequenceNode).getElement(i) }
|
||||
class UnpackingAssignmentSequenceTarget extends UnpackingAssignmentTarget instanceof SequenceNode {
|
||||
ControlFlowNode getElement(int i) { result = super.getElement(i) }
|
||||
|
||||
ControlFlowNode getAnElement() { result = this.getElement(_) }
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ private module Cached {
|
||||
DataFlowPrivate::iterableUnpackingStoreStep(nodeFrom, _, nodeTo)
|
||||
or
|
||||
awaitStep(nodeFrom, nodeTo)
|
||||
or
|
||||
asyncWithStep(nodeFrom, nodeTo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,3 +213,24 @@ predicate copyStep(DataFlow::CfgNode nodeFrom, DataFlow::CfgNode nodeTo) {
|
||||
predicate awaitStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
nodeTo.asExpr().(Await).getValue() = nodeFrom.asExpr()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if taint can flow from `nodeFrom` to `nodeTo` inside an `async with` statement.
|
||||
*
|
||||
* For example in
|
||||
* ```python
|
||||
* async with open("foo") as f:
|
||||
* ```
|
||||
* the variable `f` is tainted if the result of `open("foo")` is tainted.
|
||||
*/
|
||||
predicate asyncWithStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
exists(With with, ControlFlowNode contextManager, ControlFlowNode var |
|
||||
nodeFrom.(DataFlow::CfgNode).getNode() = contextManager and
|
||||
nodeTo.(DataFlow::EssaNode).getVar().getDefinition().(WithDefinition).getDefiningNode() = var and
|
||||
// see `with_flow` in `python/ql/src/semmle/python/dataflow/Implementation.qll`
|
||||
with.getContextExpr() = contextManager.getNode() and
|
||||
with.getOptionalVars() = var.getNode() and
|
||||
with.isAsync() and
|
||||
contextManager.strictlyDominates(var)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -52,6 +52,24 @@ private module Cached {
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets the summary resulting from prepending `step` to this type-tracking summary. */
|
||||
cached
|
||||
TypeBackTracker prepend(TypeBackTracker tbt, StepSummary step) {
|
||||
exists(Boolean hasReturn, string content | tbt = MkTypeBackTracker(hasReturn, content) |
|
||||
step = LevelStep() and result = tbt
|
||||
or
|
||||
step = CallStep() and hasReturn = false and result = tbt
|
||||
or
|
||||
step = ReturnStep() and result = MkTypeBackTracker(true, content)
|
||||
or
|
||||
exists(string p |
|
||||
step = LoadStep(p) and content = "" and result = MkTypeBackTracker(hasReturn, p)
|
||||
)
|
||||
or
|
||||
step = StoreStep(content) and result = MkTypeBackTracker(hasReturn, "")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the summary that corresponds to having taken a forwards
|
||||
* heap and/or intra-procedural step from `nodeFrom` to `nodeTo`.
|
||||
@@ -365,19 +383,7 @@ class TypeBackTracker extends TTypeBackTracker {
|
||||
TypeBackTracker() { this = MkTypeBackTracker(hasReturn, content) }
|
||||
|
||||
/** Gets the summary resulting from prepending `step` to this type-tracking summary. */
|
||||
TypeBackTracker prepend(StepSummary step) {
|
||||
step = LevelStep() and result = this
|
||||
or
|
||||
step = CallStep() and hasReturn = false and result = this
|
||||
or
|
||||
step = ReturnStep() and result = MkTypeBackTracker(true, content)
|
||||
or
|
||||
exists(string p |
|
||||
step = LoadStep(p) and content = "" and result = MkTypeBackTracker(hasReturn, p)
|
||||
)
|
||||
or
|
||||
step = StoreStep(content) and result = MkTypeBackTracker(hasReturn, "")
|
||||
}
|
||||
TypeBackTracker prepend(StepSummary step) { result = prepend(this, step) }
|
||||
|
||||
/** Gets a textual representation of this summary. */
|
||||
string toString() {
|
||||
@@ -459,6 +465,19 @@ class TypeBackTracker extends TTypeBackTracker {
|
||||
simpleLocalFlowStep(nodeFrom, nodeTo) and
|
||||
this = result
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a forwards summary that is compatible with this backwards summary.
|
||||
* That is, if this summary describes the steps needed to back-track a value
|
||||
* from `sink` to `mid`, and the result is a valid summary of the steps needed
|
||||
* to track a value from `source` to `mid`, then the value from `source` may
|
||||
* also flow to `sink`.
|
||||
*/
|
||||
TypeTracker getACompatibleTypeTracker() {
|
||||
exists(boolean hasCall | result = MkTypeTracker(hasCall, content) |
|
||||
hasCall = false or this.hasReturn() = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides predicates for implementing custom `TypeBackTracker`s. */
|
||||
|
||||
@@ -639,16 +639,14 @@ module DataFlow {
|
||||
}
|
||||
}
|
||||
|
||||
deprecated private class ConfigurationAdapter extends TaintTracking::Configuration {
|
||||
ConfigurationAdapter() { this instanceof Configuration }
|
||||
|
||||
deprecated private class ConfigurationAdapter extends TaintTracking::Configuration instanceof Configuration {
|
||||
override predicate isSource(DataFlow::Node node, TaintKind kind) {
|
||||
this.(Configuration).isSource(node.asCfgNode()) and
|
||||
Configuration.super.isSource(node.asCfgNode()) and
|
||||
kind instanceof DataFlowType
|
||||
}
|
||||
|
||||
override predicate isSink(DataFlow::Node node, TaintKind kind) {
|
||||
this.(Configuration).isSink(node.asCfgNode()) and
|
||||
Configuration.super.isSink(node.asCfgNode()) and
|
||||
kind instanceof DataFlowType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,14 @@ string munge(File sourceFile, ExternalPackage package) {
|
||||
result = "/" + sourceFile.getRelativePath() + "<|>" + package.getName() + "<|>unknown"
|
||||
}
|
||||
|
||||
abstract class ExternalPackage extends Object {
|
||||
ExternalPackage() { this instanceof ModuleObject }
|
||||
|
||||
abstract class ExternalPackage extends Object instanceof ModuleObject {
|
||||
abstract string getName();
|
||||
|
||||
abstract string getVersion();
|
||||
|
||||
Object getAttribute(string name) { result = this.(ModuleObject).attr(name) }
|
||||
Object getAttribute(string name) { result = super.attr(name) }
|
||||
|
||||
PackageObject getPackage() { result = this.(ModuleObject).getPackage() }
|
||||
PackageObject getPackage() { result = super.getPackage() }
|
||||
}
|
||||
|
||||
bindingset[text]
|
||||
|
||||
@@ -152,17 +152,17 @@ class NonLocalVariable extends SsaSourceVariable {
|
||||
}
|
||||
|
||||
override ControlFlowNode getAnImplicitUse() {
|
||||
result.(CallNode).getScope().getScope*() = this.(LocalVariable).getScope()
|
||||
result.(CallNode).getScope().getScope*() = this.scope_as_local_variable()
|
||||
}
|
||||
|
||||
override ControlFlowNode getScopeEntryDefinition() {
|
||||
exists(Function f |
|
||||
f.getScope+() = this.(LocalVariable).getScope() and
|
||||
f.getScope+() = this.scope_as_local_variable() and
|
||||
f.getEntryNode() = result
|
||||
)
|
||||
or
|
||||
not this.(LocalVariable).isParameter() and
|
||||
this.(LocalVariable).getScope().getEntryNode() = result
|
||||
this.scope_as_local_variable().getEntryNode() = result
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
@@ -215,13 +215,16 @@ class ModuleVariable extends SsaSourceVariable {
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private Scope scope_as_global_variable() { result = this.(GlobalVariable).getScope() }
|
||||
|
||||
pragma[noinline]
|
||||
CallNode global_variable_callnode() { result.getScope() = this.(GlobalVariable).getScope() }
|
||||
CallNode global_variable_callnode() { result.getScope() = this.scope_as_global_variable() }
|
||||
|
||||
pragma[noinline]
|
||||
ImportMemberNode global_variable_import() {
|
||||
result.getScope() = this.(GlobalVariable).getScope() and
|
||||
import_from_dot_in_init(result.(ImportMemberNode).getModule(this.getName()))
|
||||
result.getScope() = this.scope_as_global_variable() and
|
||||
import_from_dot_in_init(result.getModule(this.getName()))
|
||||
}
|
||||
|
||||
override ControlFlowNode getAnImplicitUse() {
|
||||
@@ -250,7 +253,7 @@ class ModuleVariable extends SsaSourceVariable {
|
||||
override ControlFlowNode getScopeEntryDefinition() {
|
||||
exists(Scope s | s.getEntryNode() = result |
|
||||
/* Module entry point */
|
||||
this.(GlobalVariable).getScope() = s
|
||||
this.scope_as_global_variable() = s
|
||||
or
|
||||
/* For implicit use of __metaclass__ when constructing class */
|
||||
class_with_global_metaclass(s, this)
|
||||
@@ -286,13 +289,13 @@ class EscapingGlobalVariable extends ModuleVariable {
|
||||
override ControlFlowNode getAnImplicitUse() {
|
||||
result = ModuleVariable.super.getAnImplicitUse()
|
||||
or
|
||||
result.(CallNode).getScope().getScope+() = this.(GlobalVariable).getScope()
|
||||
result.(CallNode).getScope().getScope+() = this.scope_as_global_variable()
|
||||
or
|
||||
result = this.innerScope().getANormalExit()
|
||||
}
|
||||
|
||||
private Scope innerScope() {
|
||||
result.getScope+() = this.(GlobalVariable).getScope() and
|
||||
result.getScope+() = this.scope_as_global_variable() and
|
||||
not result instanceof ImportTimeScope
|
||||
}
|
||||
|
||||
@@ -306,7 +309,7 @@ class EscapingGlobalVariable extends ModuleVariable {
|
||||
Scope scope_as_global_variable() { result = this.(GlobalVariable).getScope() }
|
||||
|
||||
override CallNode redefinedAtCallSite() {
|
||||
result.(CallNode).getScope().getScope*() = this.scope_as_global_variable()
|
||||
result.getScope().getScope*() = this.scope_as_global_variable()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,7 +335,7 @@ class SpecialSsaSourceVariable extends SsaSourceVariable {
|
||||
Scope scope_as_global_variable() { result = this.(GlobalVariable).getScope() }
|
||||
|
||||
override CallNode redefinedAtCallSite() {
|
||||
result.(CallNode).getScope().getScope*() = this.scope_as_global_variable()
|
||||
result.getScope().getScope*() = this.scope_as_global_variable()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -330,8 +330,8 @@ module AiohttpWebModel {
|
||||
exists(Await await, DataFlow::CallCfgNode call, DataFlow::AttrRead read |
|
||||
this.asExpr() = await
|
||||
|
|
||||
read.(DataFlow::AttrRead).getObject() = Request::instance() and
|
||||
read.(DataFlow::AttrRead).getAttributeName() = "post" and
|
||||
read.getObject() = Request::instance() and
|
||||
read.getAttributeName() = "post" and
|
||||
call.getFunction() = read and
|
||||
await.getValue() = call.asExpr()
|
||||
)
|
||||
|
||||
145
python/ql/lib/semmle/python/frameworks/Aiomysql.qll
Normal file
145
python/ql/lib/semmle/python/frameworks/Aiomysql.qll
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `aiomysql` PyPI package.
|
||||
* See
|
||||
* - https://aiomysql.readthedocs.io/en/stable/index.html
|
||||
* - https://pypi.org/project/aiomysql/
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/** Provides models for the `aiomysql` PyPI package. */
|
||||
private module Aiomysql {
|
||||
private import semmle.python.internal.Awaited
|
||||
|
||||
/**
|
||||
* A `ConectionPool` is created when the result of `aiomysql.create_pool()` is awaited.
|
||||
* See https://aiomysql.readthedocs.io/en/stable/pool.html
|
||||
*/
|
||||
API::Node connectionPool() {
|
||||
result = API::moduleImport("aiomysql").getMember("create_pool").getReturn().getAwaited()
|
||||
}
|
||||
|
||||
/**
|
||||
* A `Connection` is created when
|
||||
* - the result of `aiomysql.connect()` is awaited.
|
||||
* - the result of calling `aquire` on a `ConnectionPool` is awaited.
|
||||
* See https://aiomysql.readthedocs.io/en/stable/connection.html#connection
|
||||
*/
|
||||
API::Node connection() {
|
||||
result = API::moduleImport("aiomysql").getMember("connect").getReturn().getAwaited()
|
||||
or
|
||||
result = connectionPool().getMember("acquire").getReturn().getAwaited()
|
||||
}
|
||||
|
||||
/**
|
||||
* A `Cursor` is created when
|
||||
* - the result of calling `cursor` on a `ConnectionPool` is awaited.
|
||||
* - the result of calling `cursor` on a `Connection` is awaited.
|
||||
* See https://aiomysql.readthedocs.io/en/stable/cursors.html
|
||||
*/
|
||||
API::Node cursor() {
|
||||
result = connectionPool().getMember("cursor").getReturn().getAwaited()
|
||||
or
|
||||
result = connection().getMember("cursor").getReturn().getAwaited()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling `execute` on a `Cursor` constructs a query.
|
||||
* See https://aiomysql.readthedocs.io/en/stable/cursors.html#Cursor.execute
|
||||
*/
|
||||
class CursorExecuteCall extends SqlConstruction::Range, DataFlow::CallCfgNode {
|
||||
CursorExecuteCall() { this = cursor().getMember("execute").getACall() }
|
||||
|
||||
override DataFlow::Node getSql() { result in [this.getArg(0), this.getArgByName("operation")] }
|
||||
}
|
||||
|
||||
/**
|
||||
* This is only needed to connect the argument to the execute call with the subsequnt awaiting.
|
||||
* It should be obsolete once we have `API::CallNode` available.
|
||||
*/
|
||||
private DataFlow::TypeTrackingNode cursorExecuteCall(DataFlow::TypeTracker t, DataFlow::Node sql) {
|
||||
// cursor created from connection
|
||||
t.start() and
|
||||
sql = result.(CursorExecuteCall).getSql()
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = cursorExecuteCall(t2, sql).track(t2, t))
|
||||
}
|
||||
|
||||
DataFlow::Node cursorExecuteCall(DataFlow::Node sql) {
|
||||
cursorExecuteCall(DataFlow::TypeTracker::end(), sql).flowsTo(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Awaiting the result of calling `execute` executes the query.
|
||||
* See https://aiomysql.readthedocs.io/en/stable/cursors.html#Cursor.execute
|
||||
*/
|
||||
class AwaitedCursorExecuteCall extends SqlExecution::Range {
|
||||
DataFlow::Node sql;
|
||||
|
||||
AwaitedCursorExecuteCall() { this = awaited(cursorExecuteCall(sql)) }
|
||||
|
||||
override DataFlow::Node getSql() { result = sql }
|
||||
}
|
||||
|
||||
/**
|
||||
* An `Engine` is created when the result of calling `aiomysql.sa.create_engine` is awaited.
|
||||
* See https://aiomysql.readthedocs.io/en/stable/sa.html#engine
|
||||
*/
|
||||
API::Node engine() {
|
||||
result =
|
||||
API::moduleImport("aiomysql")
|
||||
.getMember("sa")
|
||||
.getMember("create_engine")
|
||||
.getReturn()
|
||||
.getAwaited()
|
||||
}
|
||||
|
||||
/**
|
||||
* A `SAConnection` is created when the result of calling `aquire` on an `Engine` is awaited.
|
||||
* See https://aiomysql.readthedocs.io/en/stable/sa.html#connection
|
||||
*/
|
||||
API::Node saConnection() { result = engine().getMember("acquire").getReturn().getAwaited() }
|
||||
|
||||
/**
|
||||
* Calling `execute` on a `SAConnection` constructs a query.
|
||||
* See https://aiomysql.readthedocs.io/en/stable/sa.html#aiomysql.sa.SAConnection.execute
|
||||
*/
|
||||
class SAConnectionExecuteCall extends SqlConstruction::Range, DataFlow::CallCfgNode {
|
||||
SAConnectionExecuteCall() { this = saConnection().getMember("execute").getACall() }
|
||||
|
||||
override DataFlow::Node getSql() { result in [this.getArg(0), this.getArgByName("query")] }
|
||||
}
|
||||
|
||||
/**
|
||||
* This is only needed to connect the argument to the execute call with the subsequnt awaiting.
|
||||
* It should be obsolete once we have `API::CallNode` available.
|
||||
*/
|
||||
private DataFlow::TypeTrackingNode saConnectionExecuteCall(
|
||||
DataFlow::TypeTracker t, DataFlow::Node sql
|
||||
) {
|
||||
// saConnection created from engine
|
||||
t.start() and
|
||||
sql = result.(SAConnectionExecuteCall).getSql()
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = saConnectionExecuteCall(t2, sql).track(t2, t))
|
||||
}
|
||||
|
||||
DataFlow::Node saConnectionExecuteCall(DataFlow::Node sql) {
|
||||
saConnectionExecuteCall(DataFlow::TypeTracker::end(), sql).flowsTo(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Awaiting the result of calling `execute` executes the query.
|
||||
* See https://aiomysql.readthedocs.io/en/stable/sa.html#aiomysql.sa.SAConnection.execute
|
||||
*/
|
||||
class AwaitedSAConnectionExecuteCall extends SqlExecution::Range {
|
||||
DataFlow::Node sql;
|
||||
|
||||
AwaitedSAConnectionExecuteCall() { this = awaited(saConnectionExecuteCall(sql)) }
|
||||
|
||||
override DataFlow::Node getSql() { result = sql }
|
||||
}
|
||||
}
|
||||
141
python/ql/lib/semmle/python/frameworks/Aiopg.qll
Normal file
141
python/ql/lib/semmle/python/frameworks/Aiopg.qll
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `aiopg` PyPI package.
|
||||
* See
|
||||
* - https://aiopg.readthedocs.io/en/stable/index.html
|
||||
* - https://pypi.org/project/aiopg/
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/** Provides models for the `aiopg` PyPI package. */
|
||||
private module Aiopg {
|
||||
private import semmle.python.internal.Awaited
|
||||
|
||||
/**
|
||||
* A `ConectionPool` is created when the result of `aiopg.create_pool()` is awaited.
|
||||
* See https://aiopg.readthedocs.io/en/stable/core.html#pool
|
||||
*/
|
||||
API::Node connectionPool() {
|
||||
result = API::moduleImport("aiopg").getMember("create_pool").getReturn().getAwaited()
|
||||
}
|
||||
|
||||
/**
|
||||
* A `Connection` is created when
|
||||
* - the result of `aiopg.connect()` is awaited.
|
||||
* - the result of calling `aquire` on a `ConnectionPool` is awaited.
|
||||
* See https://aiopg.readthedocs.io/en/stable/core.html#connection
|
||||
*/
|
||||
API::Node connection() {
|
||||
result = API::moduleImport("aiopg").getMember("connect").getReturn().getAwaited()
|
||||
or
|
||||
result = connectionPool().getMember("acquire").getReturn().getAwaited()
|
||||
}
|
||||
|
||||
/**
|
||||
* A `Cursor` is created when
|
||||
* - the result of calling `cursor` on a `ConnectionPool` is awaited.
|
||||
* - the result of calling `cursor` on a `Connection` is awaited.
|
||||
* See https://aiopg.readthedocs.io/en/stable/core.html#cursor
|
||||
*/
|
||||
API::Node cursor() {
|
||||
result = connectionPool().getMember("cursor").getReturn().getAwaited()
|
||||
or
|
||||
result = connection().getMember("cursor").getReturn().getAwaited()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling `execute` on a `Cursor` constructs a query.
|
||||
* See https://aiopg.readthedocs.io/en/stable/core.html#aiopg.Cursor.execute
|
||||
*/
|
||||
class CursorExecuteCall extends SqlConstruction::Range, DataFlow::CallCfgNode {
|
||||
CursorExecuteCall() { this = cursor().getMember("execute").getACall() }
|
||||
|
||||
override DataFlow::Node getSql() { result in [this.getArg(0), this.getArgByName("operation")] }
|
||||
}
|
||||
|
||||
/**
|
||||
* This is only needed to connect the argument to the execute call with the subsequnt awaiting.
|
||||
* It should be obsolete once we have `API::CallNode` available.
|
||||
*/
|
||||
private DataFlow::TypeTrackingNode cursorExecuteCall(DataFlow::TypeTracker t, DataFlow::Node sql) {
|
||||
// cursor created from connection
|
||||
t.start() and
|
||||
sql = result.(CursorExecuteCall).getSql()
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = cursorExecuteCall(t2, sql).track(t2, t))
|
||||
}
|
||||
|
||||
DataFlow::Node cursorExecuteCall(DataFlow::Node sql) {
|
||||
cursorExecuteCall(DataFlow::TypeTracker::end(), sql).flowsTo(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Awaiting the result of calling `execute` executes the query.
|
||||
* See https://aiopg.readthedocs.io/en/stable/core.html#aiopg.Cursor.execute
|
||||
*/
|
||||
class AwaitedCursorExecuteCall extends SqlExecution::Range {
|
||||
DataFlow::Node sql;
|
||||
|
||||
AwaitedCursorExecuteCall() { this = awaited(cursorExecuteCall(sql)) }
|
||||
|
||||
override DataFlow::Node getSql() { result = sql }
|
||||
}
|
||||
|
||||
/**
|
||||
* An `Engine` is created when the result of calling `aiopg.sa.create_engine` is awaited.
|
||||
* See https://aiopg.readthedocs.io/en/stable/sa.html#engine
|
||||
*/
|
||||
API::Node engine() {
|
||||
result =
|
||||
API::moduleImport("aiopg").getMember("sa").getMember("create_engine").getReturn().getAwaited()
|
||||
}
|
||||
|
||||
/**
|
||||
* A `SAConnection` is created when the result of calling `aquire` on an `Engine` is awaited.
|
||||
* See https://aiopg.readthedocs.io/en/stable/sa.html#connection
|
||||
*/
|
||||
API::Node saConnection() { result = engine().getMember("acquire").getReturn().getAwaited() }
|
||||
|
||||
/**
|
||||
* Calling `execute` on a `SAConnection` constructs a query.
|
||||
* See https://aiopg.readthedocs.io/en/stable/sa.html#aiopg.sa.SAConnection.execute
|
||||
*/
|
||||
class SAConnectionExecuteCall extends SqlConstruction::Range, DataFlow::CallCfgNode {
|
||||
SAConnectionExecuteCall() { this = saConnection().getMember("execute").getACall() }
|
||||
|
||||
override DataFlow::Node getSql() { result in [this.getArg(0), this.getArgByName("query")] }
|
||||
}
|
||||
|
||||
/**
|
||||
* This is only needed to connect the argument to the execute call with the subsequnt awaiting.
|
||||
* It should be obsolete once we have `API::CallNode` available.
|
||||
*/
|
||||
private DataFlow::TypeTrackingNode saConnectionExecuteCall(
|
||||
DataFlow::TypeTracker t, DataFlow::Node sql
|
||||
) {
|
||||
// saConnection created from engine
|
||||
t.start() and
|
||||
sql = result.(SAConnectionExecuteCall).getSql()
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = saConnectionExecuteCall(t2, sql).track(t2, t))
|
||||
}
|
||||
|
||||
DataFlow::Node saConnectionExecuteCall(DataFlow::Node sql) {
|
||||
saConnectionExecuteCall(DataFlow::TypeTracker::end(), sql).flowsTo(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Awaiting the result of calling `execute` executes the query.
|
||||
* See https://aiopg.readthedocs.io/en/stable/sa.html#aiopg.sa.SAConnection.execute
|
||||
*/
|
||||
class AwaitedSAConnectionExecuteCall extends SqlExecution::Range {
|
||||
DataFlow::Node sql;
|
||||
|
||||
AwaitedSAConnectionExecuteCall() { this = awaited(saConnectionExecuteCall(sql)) }
|
||||
|
||||
override DataFlow::Node getSql() { result = sql }
|
||||
}
|
||||
}
|
||||
162
python/ql/lib/semmle/python/frameworks/Asyncpg.qll
Normal file
162
python/ql/lib/semmle/python/frameworks/Asyncpg.qll
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `asyncpg` PyPI package.
|
||||
* See https://magicstack.github.io/asyncpg/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/** Provides models for the `asyncpg` PyPI package. */
|
||||
private module Asyncpg {
|
||||
private import semmle.python.internal.Awaited
|
||||
|
||||
/** A `ConectionPool` is created when the result of `asyncpg.create_pool()` is awaited. */
|
||||
API::Node connectionPool() {
|
||||
result = API::moduleImport("asyncpg").getMember("create_pool").getReturn().getAwaited()
|
||||
}
|
||||
|
||||
/**
|
||||
* A `Connection` is created when
|
||||
* - the result of `asyncpg.connect()` is awaited.
|
||||
* - the result of calling `aquire` on a `ConnectionPool` is awaited.
|
||||
*/
|
||||
API::Node connection() {
|
||||
result = API::moduleImport("asyncpg").getMember("connect").getReturn().getAwaited()
|
||||
or
|
||||
result = connectionPool().getMember("acquire").getReturn().getAwaited()
|
||||
}
|
||||
|
||||
/** `Connection`s and `ConnectionPool`s provide some methods that execute SQL. */
|
||||
class SqlExecutionOnConnection extends SqlExecution::Range, DataFlow::MethodCallNode {
|
||||
string methodName;
|
||||
|
||||
SqlExecutionOnConnection() {
|
||||
methodName in ["copy_from_query", "execute", "fetch", "fetchrow", "fetchval", "executemany"] and
|
||||
this.calls([connectionPool().getAUse(), connection().getAUse()], methodName)
|
||||
}
|
||||
|
||||
override DataFlow::Node getSql() {
|
||||
methodName in ["copy_from_query", "execute", "fetch", "fetchrow", "fetchval"] and
|
||||
result in [this.getArg(0), this.getArgByName("query")]
|
||||
or
|
||||
methodName = "executemany" and
|
||||
result in [this.getArg(0), this.getArgByName("command")]
|
||||
}
|
||||
}
|
||||
|
||||
/** `Connection`s and `ConnectionPool`s provide some methods that access the file system. */
|
||||
class FileAccessOnConnection extends FileSystemAccess::Range, DataFlow::MethodCallNode {
|
||||
string methodName;
|
||||
|
||||
FileAccessOnConnection() {
|
||||
methodName in ["copy_from_query", "copy_from_table", "copy_to_table"] and
|
||||
this.calls([connectionPool().getAUse(), connection().getAUse()], methodName)
|
||||
}
|
||||
|
||||
// The path argument is keyword only.
|
||||
override DataFlow::Node getAPathArgument() {
|
||||
methodName in ["copy_from_query", "copy_from_table"] and
|
||||
result = this.getArgByName("output")
|
||||
or
|
||||
methodName = "copy_to_table" and
|
||||
result = this.getArgByName("source")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models of the `PreparedStatement` class in `asyncpg`.
|
||||
* `PreparedStatement`s are created when the result of calling `prepare(query)` on a connection is awaited.
|
||||
* The result of calling `prepare(query)` is a `PreparedStatementFactory` and the argument, `query` needs to
|
||||
* be tracked to the place where a `PreparedStatement` is created and then futher to any executing methods.
|
||||
* Hence the two type trackers.
|
||||
*
|
||||
* TODO: Rewrite this, once we have `API::CallNode` available.
|
||||
*/
|
||||
module PreparedStatement {
|
||||
class PreparedStatementConstruction extends SqlConstruction::Range, DataFlow::CallCfgNode {
|
||||
PreparedStatementConstruction() { this = connection().getMember("prepare").getACall() }
|
||||
|
||||
override DataFlow::Node getSql() { result in [this.getArg(0), this.getArgByName("query")] }
|
||||
}
|
||||
|
||||
private DataFlow::TypeTrackingNode preparedStatementFactory(
|
||||
DataFlow::TypeTracker t, DataFlow::Node sql
|
||||
) {
|
||||
t.start() and
|
||||
sql = result.(PreparedStatementConstruction).getSql()
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = preparedStatementFactory(t2, sql).track(t2, t))
|
||||
}
|
||||
|
||||
DataFlow::Node preparedStatementFactory(DataFlow::Node sql) {
|
||||
preparedStatementFactory(DataFlow::TypeTracker::end(), sql).flowsTo(result)
|
||||
}
|
||||
|
||||
private DataFlow::TypeTrackingNode preparedStatement(DataFlow::TypeTracker t, DataFlow::Node sql) {
|
||||
t.start() and
|
||||
result = awaited(preparedStatementFactory(sql))
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = preparedStatement(t2, sql).track(t2, t))
|
||||
}
|
||||
|
||||
DataFlow::Node preparedStatement(DataFlow::Node sql) {
|
||||
preparedStatement(DataFlow::TypeTracker::end(), sql).flowsTo(result)
|
||||
}
|
||||
|
||||
class PreparedStatementExecution extends SqlExecution::Range, DataFlow::MethodCallNode {
|
||||
DataFlow::Node sql;
|
||||
|
||||
PreparedStatementExecution() {
|
||||
this.calls(preparedStatement(sql), ["executemany", "fetch", "fetchrow", "fetchval"])
|
||||
}
|
||||
|
||||
override DataFlow::Node getSql() { result = sql }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models of the `Cursor` class in `asyncpg`.
|
||||
* `Cursor`s are created
|
||||
* - when the result of calling `cursor(query)` on a connection is awaited.
|
||||
* - when the result of calling `cursor()` on a prepared statement is awaited.
|
||||
* The result of calling `cursor` in either case is a `CursorFactory` and the argument, `query` needs to
|
||||
* be tracked to the place where a `Cursor` is created, hence the type tracker.
|
||||
* The creation of the `Cursor` executes the query.
|
||||
*
|
||||
* TODO: Rewrite this, once we have `API::CallNode` available.
|
||||
*/
|
||||
module Cursor {
|
||||
class CursorConstruction extends SqlConstruction::Range, DataFlow::CallCfgNode {
|
||||
CursorConstruction() { this = connection().getMember("cursor").getACall() }
|
||||
|
||||
override DataFlow::Node getSql() { result in [this.getArg(0), this.getArgByName("query")] }
|
||||
}
|
||||
|
||||
private DataFlow::TypeTrackingNode cursorFactory(DataFlow::TypeTracker t, DataFlow::Node sql) {
|
||||
// cursor created from connection
|
||||
t.start() and
|
||||
sql = result.(CursorConstruction).getSql()
|
||||
or
|
||||
// cursor created from prepared statement
|
||||
t.start() and
|
||||
result.(DataFlow::MethodCallNode).calls(PreparedStatement::preparedStatement(sql), "cursor")
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = cursorFactory(t2, sql).track(t2, t))
|
||||
}
|
||||
|
||||
DataFlow::Node cursorFactory(DataFlow::Node sql) {
|
||||
cursorFactory(DataFlow::TypeTracker::end(), sql).flowsTo(result)
|
||||
}
|
||||
|
||||
/** The creation of a `Cursor` executes the associated query. */
|
||||
class CursorCreation extends SqlExecution::Range {
|
||||
DataFlow::Node sql;
|
||||
|
||||
CursorCreation() { this = awaited(cursorFactory(sql)) }
|
||||
|
||||
override DataFlow::Node getSql() { result = sql }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,12 @@ private import semmle.python.frameworks.internal.SelfRefMixin
|
||||
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
|
||||
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Provides models for the `django` PyPI package.
|
||||
* See https://www.djangoproject.com/.
|
||||
*/
|
||||
private module Django {
|
||||
module Django {
|
||||
/** Provides models for the `django.views` module */
|
||||
module Views {
|
||||
/**
|
||||
@@ -367,6 +369,52 @@ private module Django {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `django.contrib.auth.models.User` class
|
||||
*
|
||||
* See https://docs.djangoproject.com/en/3.2/ref/contrib/auth/#user-model.
|
||||
*/
|
||||
module User {
|
||||
/**
|
||||
* A source of instances of `django.contrib.auth.models.User`, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `User::instance()` to get references to instances of `django.contrib.auth.models.User`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** Gets a reference to an instance of `django.contrib.auth.models.User`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `django.contrib.auth.models.User`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* Taint propagation for `django.contrib.auth.models.User`.
|
||||
*/
|
||||
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
|
||||
InstanceTaintSteps() { this = "django.contrib.auth.models.User" }
|
||||
|
||||
override DataFlow::Node getInstance() { result = instance() }
|
||||
|
||||
override string getAttributeName() {
|
||||
result in ["username", "first_name", "last_name", "email"]
|
||||
}
|
||||
|
||||
override string getMethodName() { none() }
|
||||
|
||||
override string getAsyncMethodName() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `django.core.files.uploadedfile.UploadedFile` class
|
||||
*
|
||||
@@ -466,10 +514,12 @@ private module Django {
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Provides models for the `django` PyPI package (that we are not quite ready to publicly expose yet).
|
||||
* See https://www.djangoproject.com/.
|
||||
*/
|
||||
private module PrivateDjango {
|
||||
module PrivateDjango {
|
||||
// ---------------------------------------------------------------------------
|
||||
// django
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -496,6 +546,7 @@ private module PrivateDjango {
|
||||
/** Gets a reference to the `django.db.connection` object. */
|
||||
API::Node connection() { result = db().getMember("connection") }
|
||||
|
||||
/** A `django.db.connection` is a PEP249 compliant DB connection. */
|
||||
class DjangoDbConnection extends PEP249::Connection::InstanceSource {
|
||||
DjangoDbConnection() { this = connection().getAUse() }
|
||||
}
|
||||
@@ -692,6 +743,7 @@ private module PrivateDjango {
|
||||
|
||||
/** Provides models for the `django.conf` module */
|
||||
module conf {
|
||||
/** Provides models for the `django.conf.urls` module */
|
||||
module conf_urls {
|
||||
// -------------------------------------------------------------------------
|
||||
// django.conf.urls
|
||||
@@ -890,6 +942,7 @@ private module PrivateDjango {
|
||||
* See https://docs.djangoproject.com/en/3.1/ref/request-response/#django.http.HttpResponse.
|
||||
*/
|
||||
module HttpResponse {
|
||||
/** Gets a reference to the `django.http.response.HttpResponse` class. */
|
||||
API::Node baseClassRef() {
|
||||
result = response().getMember("HttpResponse")
|
||||
or
|
||||
@@ -897,7 +950,7 @@ private module PrivateDjango {
|
||||
result = http().getMember("HttpResponse")
|
||||
}
|
||||
|
||||
/** Gets a reference to the `django.http.response.HttpResponse` class. */
|
||||
/** Gets a reference to the `django.http.response.HttpResponse` class or any subclass. */
|
||||
API::Node classRef() { result = baseClassRef().getASubclass*() }
|
||||
|
||||
/**
|
||||
@@ -1893,14 +1946,11 @@ private module PrivateDjango {
|
||||
* with the django framework.
|
||||
*
|
||||
* Most functions take a django HttpRequest as a parameter (but not all).
|
||||
*
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `DjangoRouteHandler::Range` instead.
|
||||
*/
|
||||
private class DjangoRouteHandler extends Function {
|
||||
DjangoRouteHandler() {
|
||||
exists(DjangoRouteSetup route | route.getViewArg() = poorMansFunctionTracker(this))
|
||||
or
|
||||
any(DjangoViewClass vc).getARequestHandler() = this
|
||||
}
|
||||
|
||||
class DjangoRouteHandler extends Function instanceof DjangoRouteHandler::Range {
|
||||
/**
|
||||
* Gets the index of the parameter where the first routed parameter can be passed --
|
||||
* that is, the one just after any possible `self` or HttpRequest parameters.
|
||||
@@ -1920,6 +1970,24 @@ private module PrivateDjango {
|
||||
Parameter getRequestParam() { result = this.getArg(this.getRequestParamIndex()) }
|
||||
}
|
||||
|
||||
/** Provides a class for modeling new django route handlers. */
|
||||
module DjangoRouteHandler {
|
||||
/**
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `DjangoRouteHandler` instead.
|
||||
*/
|
||||
abstract class Range extends Function { }
|
||||
|
||||
/** Route handlers from normal usage of django. */
|
||||
private class StandardDjangoRouteHandlers extends Range {
|
||||
StandardDjangoRouteHandlers() {
|
||||
exists(DjangoRouteSetup route | route.getViewArg() = poorMansFunctionTracker(this))
|
||||
or
|
||||
any(DjangoViewClass vc).getARequestHandler() = this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A method named `get_redirect_url` on a django view class.
|
||||
*
|
||||
@@ -1941,7 +2009,7 @@ private module PrivateDjango {
|
||||
}
|
||||
|
||||
/** A data-flow node that sets up a route on a server, using the django framework. */
|
||||
abstract private class DjangoRouteSetup extends HTTP::Server::RouteSetup::Range, DataFlow::CfgNode {
|
||||
abstract class DjangoRouteSetup extends HTTP::Server::RouteSetup::Range, DataFlow::CfgNode {
|
||||
/** Gets the data-flow node that is used as the argument for the view handler. */
|
||||
abstract DataFlow::Node getViewArg();
|
||||
|
||||
|
||||
352
python/ql/lib/semmle/python/frameworks/FastApi.qll
Normal file
352
python/ql/lib/semmle/python/frameworks/FastApi.qll
Normal file
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `fastapi` PyPI package.
|
||||
* See https://fastapi.tiangolo.com/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.frameworks.Pydantic
|
||||
private import semmle.python.frameworks.Starlette
|
||||
|
||||
/**
|
||||
* Provides models for the `fastapi` PyPI package.
|
||||
* See https://fastapi.tiangolo.com/.
|
||||
*/
|
||||
private module FastApi {
|
||||
/**
|
||||
* Provides models for FastAPI applications (an instance of `fastapi.FastAPI`).
|
||||
*/
|
||||
module App {
|
||||
/** Gets a reference to a FastAPI application (an instance of `fastapi.FastAPI`). */
|
||||
API::Node instance() { result = API::moduleImport("fastapi").getMember("FastAPI").getReturn() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `fastapi.APIRouter` class
|
||||
*
|
||||
* See https://fastapi.tiangolo.com/tutorial/bigger-applications/.
|
||||
*/
|
||||
module APIRouter {
|
||||
/** Gets a reference to an instance of `fastapi.APIRouter`. */
|
||||
API::Node instance() {
|
||||
result = API::moduleImport("fastapi").getMember("APIRouter").getReturn()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// routing modeling
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* A call to a method like `get` or `post` on a FastAPI application.
|
||||
*
|
||||
* See https://fastapi.tiangolo.com/tutorial/first-steps/#define-a-path-operation-decorator
|
||||
*/
|
||||
private class FastApiRouteSetup extends HTTP::Server::RouteSetup::Range, DataFlow::CallCfgNode {
|
||||
FastApiRouteSetup() {
|
||||
exists(string routeAddingMethod |
|
||||
routeAddingMethod = HTTP::httpVerbLower()
|
||||
or
|
||||
routeAddingMethod in ["api_route", "websocket"]
|
||||
|
|
||||
this = App::instance().getMember(routeAddingMethod).getACall()
|
||||
or
|
||||
this = APIRouter::instance().getMember(routeAddingMethod).getACall()
|
||||
)
|
||||
}
|
||||
|
||||
override Parameter getARoutedParameter() {
|
||||
// this will need to be refined a bit, since you can add special parameters to
|
||||
// your request handler functions that are used to pass in the response. There
|
||||
// might be other special cases as well, but as a start this is not too far off
|
||||
// the mark.
|
||||
result = this.getARequestHandler().getArgByName(_) and
|
||||
// type-annotated with `Response`
|
||||
not any(Response::RequestHandlerParam src).asExpr() = result
|
||||
}
|
||||
|
||||
override DataFlow::Node getUrlPatternArg() {
|
||||
result in [this.getArg(0), this.getArgByName("path")]
|
||||
}
|
||||
|
||||
override Function getARequestHandler() { result.getADecorator().getAFlowNode() = node }
|
||||
|
||||
override string getFramework() { result = "FastAPI" }
|
||||
|
||||
/** Gets the argument specifying the response class to use, if any. */
|
||||
DataFlow::Node getResponseClassArg() { result = this.getArgByName("response_class") }
|
||||
}
|
||||
|
||||
/**
|
||||
* A parameter to a request handler that has a type-annotation with a class that is a
|
||||
* Pydantic model.
|
||||
*/
|
||||
private class PydanticModelRequestHandlerParam extends Pydantic::BaseModel::InstanceSource,
|
||||
DataFlow::ParameterNode {
|
||||
PydanticModelRequestHandlerParam() {
|
||||
this.getParameter().getAnnotation() = Pydantic::BaseModel::subclassRef().getAUse().asExpr() and
|
||||
any(FastApiRouteSetup rs).getARequestHandler().getArgByName(_) = this.getParameter()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Response modeling
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* A parameter to a request handler that has a WebSocket type-annotation.
|
||||
*/
|
||||
private class WebSocketRequestHandlerParam extends Starlette::WebSocket::InstanceSource,
|
||||
DataFlow::ParameterNode {
|
||||
WebSocketRequestHandlerParam() {
|
||||
this.getParameter().getAnnotation() = Starlette::WebSocket::classRef().getAUse().asExpr() and
|
||||
any(FastApiRouteSetup rs).getARequestHandler().getArgByName(_) = this.getParameter()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `fastapi.Response` class and subclasses.
|
||||
*
|
||||
* See https://fastapi.tiangolo.com/advanced/custom-response/#response.
|
||||
*/
|
||||
module Response {
|
||||
/**
|
||||
* Gets the `API::Node` for the manually modeled response classes called `name`.
|
||||
*/
|
||||
private API::Node getModeledResponseClass(string name) {
|
||||
name = "Response" and
|
||||
result = API::moduleImport("fastapi").getMember(name)
|
||||
or
|
||||
// see https://github.com/tiangolo/fastapi/blob/master/fastapi/responses.py
|
||||
name in [
|
||||
"Response", "HTMLResponse", "PlainTextResponse", "JSONResponse", "UJSONResponse",
|
||||
"ORJSONResponse", "RedirectResponse", "StreamingResponse", "FileResponse"
|
||||
] and
|
||||
result = API::moduleImport("fastapi").getMember("responses").getMember(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default MIME type for a FastAPI response class (defined with the
|
||||
* `media_type` class-attribute).
|
||||
*
|
||||
* Also models user-defined classes and tries to take inheritance into account.
|
||||
*
|
||||
* TODO: build easy way to solve problems like this, like we used to have the
|
||||
* `ClassValue.lookup` predicate.
|
||||
*/
|
||||
private string getDefaultMimeType(API::Node responseClass) {
|
||||
exists(string name | responseClass = getModeledResponseClass(name) |
|
||||
// no defaults for these.
|
||||
name in ["Response", "RedirectResponse", "StreamingResponse"] and
|
||||
none()
|
||||
or
|
||||
// For `FileResponse` the code will guess what mimetype
|
||||
// to use, or fall back to "text/plain", but claiming that all responses will
|
||||
// have "text/plain" per default is also highly inaccurate, so just going to not
|
||||
// do anything about this.
|
||||
name = "FileResponse" and
|
||||
none()
|
||||
or
|
||||
name = "HTMLResponse" and
|
||||
result = "text/html"
|
||||
or
|
||||
name = "PlainTextResponse" and
|
||||
result = "text/plain"
|
||||
or
|
||||
name in ["JSONResponse", "UJSONResponse", "ORJSONResponse"] and
|
||||
result = "application/json"
|
||||
)
|
||||
or
|
||||
// user-defined subclasses
|
||||
exists(Class cls, API::Node base |
|
||||
base = getModeledResponseClass(_).getASubclass*() and
|
||||
cls.getABase() = base.getAUse().asExpr() and
|
||||
responseClass.getAnImmediateUse().asExpr().(ClassExpr) = cls.getParent()
|
||||
|
|
||||
exists(Assign assign | assign = cls.getAStmt() |
|
||||
assign.getATarget().(Name).getId() = "media_type" and
|
||||
result = assign.getValue().(StrConst).getText()
|
||||
)
|
||||
or
|
||||
// TODO: this should use a proper MRO calculation instead
|
||||
not exists(Assign assign | assign = cls.getAStmt() |
|
||||
assign.getATarget().(Name).getId() = "media_type"
|
||||
) and
|
||||
result = getDefaultMimeType(base)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `fastapi.Response` and its' subclasses, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `Response::instance()` to get references to instances of `fastapi.Response`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** A direct instantiation of a response class. */
|
||||
private class ResponseInstantiation extends InstanceSource, HTTP::Server::HttpResponse::Range,
|
||||
DataFlow::CallCfgNode {
|
||||
API::Node baseApiNode;
|
||||
API::Node responseClass;
|
||||
|
||||
ResponseInstantiation() {
|
||||
baseApiNode = getModeledResponseClass(_) and
|
||||
responseClass = baseApiNode.getASubclass*() and
|
||||
this = responseClass.getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() {
|
||||
not baseApiNode = getModeledResponseClass(["RedirectResponse", "FileResponse"]) and
|
||||
result in [this.getArg(0), this.getArgByName("content")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() {
|
||||
not baseApiNode = getModeledResponseClass("RedirectResponse") and
|
||||
result in [this.getArg(3), this.getArgByName("media_type")]
|
||||
}
|
||||
|
||||
override string getMimetypeDefault() { result = getDefaultMimeType(responseClass) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A direct instantiation of a redirect response.
|
||||
*/
|
||||
private class RedirectResponseInstantiation extends ResponseInstantiation,
|
||||
HTTP::Server::HttpRedirectResponse::Range {
|
||||
RedirectResponseInstantiation() { baseApiNode = getModeledResponseClass("RedirectResponse") }
|
||||
|
||||
override DataFlow::Node getRedirectLocation() {
|
||||
result in [this.getArg(0), this.getArgByName("url")]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implicit response from a return of FastAPI request handler.
|
||||
*/
|
||||
private class FastApiRequestHandlerReturn extends HTTP::Server::HttpResponse::Range,
|
||||
DataFlow::CfgNode {
|
||||
FastApiRouteSetup routeSetup;
|
||||
|
||||
FastApiRequestHandlerReturn() {
|
||||
node = routeSetup.getARequestHandler().getAReturnValueFlowNode()
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() { result = this }
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
|
||||
|
||||
override string getMimetypeDefault() {
|
||||
exists(API::Node responseClass |
|
||||
responseClass.getAUse() = routeSetup.getResponseClassArg() and
|
||||
result = getDefaultMimeType(responseClass)
|
||||
)
|
||||
or
|
||||
not exists(routeSetup.getResponseClassArg()) and
|
||||
result = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implicit response from a return of FastAPI request handler, that has
|
||||
* `response_class` set to a `FileResponse`.
|
||||
*/
|
||||
private class FastApiRequestHandlerFileResponseReturn extends FastApiRequestHandlerReturn {
|
||||
FastApiRequestHandlerFileResponseReturn() {
|
||||
exists(API::Node responseClass |
|
||||
responseClass.getAUse() = routeSetup.getResponseClassArg() and
|
||||
responseClass = getModeledResponseClass("FileResponse").getASubclass*()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* An implicit response from a return of FastAPI request handler, that has
|
||||
* `response_class` set to a `RedirectResponse`.
|
||||
*/
|
||||
private class FastApiRequestHandlerRedirectReturn extends FastApiRequestHandlerReturn,
|
||||
HTTP::Server::HttpRedirectResponse::Range {
|
||||
FastApiRequestHandlerRedirectReturn() {
|
||||
exists(API::Node responseClass |
|
||||
responseClass.getAUse() = routeSetup.getResponseClassArg() and
|
||||
responseClass = getModeledResponseClass("RedirectResponse").getASubclass*()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() { none() }
|
||||
|
||||
override DataFlow::Node getRedirectLocation() { result = this }
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* A parameter to a FastAPI request-handler that has a `fastapi.Response`
|
||||
* type-annotation.
|
||||
*/
|
||||
class RequestHandlerParam extends InstanceSource, DataFlow::ParameterNode {
|
||||
RequestHandlerParam() {
|
||||
this.getParameter().getAnnotation() =
|
||||
getModeledResponseClass(_).getASubclass*().getAUse().asExpr() and
|
||||
any(FastApiRouteSetup rs).getARequestHandler().getArgByName(_) = this.getParameter()
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `fastapi.Response`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `fastapi.Response`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* A call to `set_cookie` on a FastAPI Response.
|
||||
*/
|
||||
private class SetCookieCall extends HTTP::Server::CookieWrite::Range, DataFlow::MethodCallNode {
|
||||
SetCookieCall() { this.calls(instance(), "set_cookie") }
|
||||
|
||||
override DataFlow::Node getHeaderArg() { none() }
|
||||
|
||||
override DataFlow::Node getNameArg() { result in [this.getArg(0), this.getArgByName("key")] }
|
||||
|
||||
override DataFlow::Node getValueArg() {
|
||||
result in [this.getArg(1), this.getArgByName("value")]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `append` on a `headers` of a FastAPI Response, with the `Set-Cookie`
|
||||
* header-key.
|
||||
*/
|
||||
private class HeadersAppendCookie extends HTTP::Server::CookieWrite::Range,
|
||||
DataFlow::MethodCallNode {
|
||||
HeadersAppendCookie() {
|
||||
exists(DataFlow::AttrRead headers, DataFlow::Node keyArg |
|
||||
headers.accesses(instance(), "headers") and
|
||||
this.calls(headers, "append") and
|
||||
keyArg in [this.getArg(0), this.getArgByName("key")] and
|
||||
keyArg.getALocalSource().asExpr().(StrConst).getText().toLowerCase() = "set-cookie"
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getHeaderArg() {
|
||||
result in [this.getArg(1), this.getArgByName("value")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { none() }
|
||||
|
||||
override DataFlow::Node getValueArg() { none() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ private import semmle.python.Concepts
|
||||
private import semmle.python.frameworks.Werkzeug
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
|
||||
private import semmle.python.security.dataflow.PathInjectionCustomizations
|
||||
|
||||
/**
|
||||
* Provides models for the `flask` PyPI package.
|
||||
@@ -73,7 +74,11 @@ module Flask {
|
||||
*/
|
||||
module Blueprint {
|
||||
/** Gets a reference to the `flask.Blueprint` class. */
|
||||
API::Node classRef() { result = API::moduleImport("flask").getMember("Blueprint") }
|
||||
API::Node classRef() {
|
||||
result = API::moduleImport("flask").getMember("Blueprint")
|
||||
or
|
||||
result = API::moduleImport("flask").getMember("blueprints").getMember("Blueprint")
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `flask.Blueprint`. */
|
||||
API::Node instance() { result = classRef().getReturn() }
|
||||
@@ -233,7 +238,7 @@ module Flask {
|
||||
}
|
||||
|
||||
/** A route setup made by flask (sharing handling of URL patterns). */
|
||||
abstract private class FlaskRouteSetup extends HTTP::Server::RouteSetup::Range {
|
||||
abstract class FlaskRouteSetup extends HTTP::Server::RouteSetup::Range {
|
||||
override Parameter getARoutedParameter() {
|
||||
// If we don't know the URL pattern, we simply mark all parameters as a routed
|
||||
// parameter. This should give us more RemoteFlowSources but could also lead to
|
||||
@@ -525,13 +530,30 @@ module Flask {
|
||||
*
|
||||
* See https://flask.palletsprojects.com/en/1.1.x/api/#flask.send_from_directory
|
||||
*/
|
||||
class FlaskSendFromDirectory extends FileSystemAccess::Range, DataFlow::CallCfgNode {
|
||||
FlaskSendFromDirectory() {
|
||||
private class FlaskSendFromDirectoryCall extends FileSystemAccess::Range, DataFlow::CallCfgNode {
|
||||
FlaskSendFromDirectoryCall() {
|
||||
this = API::moduleImport("flask").getMember("send_from_directory").getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAPathArgument() {
|
||||
result in [this.getArg(_), this.getArgByName(["directory", "filename"])]
|
||||
result in [
|
||||
this.getArg(0), this.getArgByName("directory"),
|
||||
// as described in the docs, the `filename` argument is restrained to be within
|
||||
// the provided directory, so is not exposed to path-injection. (but is still a
|
||||
// path-argument).
|
||||
this.getArg(1), this.getArgByName("filename")
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To exclude `filename` argument to `flask.send_from_directory` as a path-injection sink.
|
||||
*/
|
||||
private class FlaskSendFromDirectoryCallFilenameSanitizer extends PathInjection::Sanitizer {
|
||||
FlaskSendFromDirectoryCallFilenameSanitizer() {
|
||||
this = any(FlaskSendFromDirectoryCall c).getArg(1)
|
||||
or
|
||||
this = any(FlaskSendFromDirectoryCall c).getArgByName("filename")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,8 +562,8 @@ module Flask {
|
||||
*
|
||||
* See https://flask.palletsprojects.com/en/1.1.x/api/#flask.send_file
|
||||
*/
|
||||
class FlaskSendFile extends FileSystemAccess::Range, DataFlow::CallCfgNode {
|
||||
FlaskSendFile() { this = API::moduleImport("flask").getMember("send_file").getACall() }
|
||||
private class FlaskSendFileCall extends FileSystemAccess::Range, DataFlow::CallCfgNode {
|
||||
FlaskSendFileCall() { this = API::moduleImport("flask").getMember("send_file").getACall() }
|
||||
|
||||
override DataFlow::Node getAPathArgument() {
|
||||
result in [this.getArg(0), this.getArgByName("filename_or_fp")]
|
||||
|
||||
79
python/ql/lib/semmle/python/frameworks/FlaskAdmin.qll
Normal file
79
python/ql/lib/semmle/python/frameworks/FlaskAdmin.qll
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `Flask-Admin` PyPI package
|
||||
* (imported as `flask_admin`).
|
||||
*
|
||||
* See
|
||||
* - https://flask-admin.readthedocs.io/en/latest/
|
||||
* - https://pypi.org/project/Flask-Admin/
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.frameworks.Flask
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/**
|
||||
* Provides models for the `Flask-Admin` PyPI package (imported as `flask_admin`).
|
||||
*
|
||||
* See
|
||||
* - https://flask-admin.readthedocs.io/en/latest/
|
||||
* - https://pypi.org/project/Flask-Admin/
|
||||
*/
|
||||
private module FlaskAdmin {
|
||||
/**
|
||||
* A call to `flask_admin.expose`, which is used as a decorator to make the
|
||||
* function exposed in the admin interface (and make it a request handler)
|
||||
*
|
||||
* See https://flask-admin.readthedocs.io/en/latest/api/mod_base/#flask_admin.base.expose
|
||||
*/
|
||||
private class FlaskAdminExposeCall extends Flask::FlaskRouteSetup, DataFlow::CallCfgNode {
|
||||
FlaskAdminExposeCall() {
|
||||
this = API::moduleImport("flask_admin").getMember("expose").getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getUrlPatternArg() {
|
||||
result in [this.getArg(0), this.getArgByName("url")]
|
||||
}
|
||||
|
||||
override Function getARequestHandler() { result.getADecorator().getAFlowNode() = node }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `flask_admin.expose_plugview`, which is used as a decorator to make the
|
||||
* class (which we expect to be a flask View class) exposed in the admin interface.
|
||||
*
|
||||
* See https://flask-admin.readthedocs.io/en/latest/api/mod_base/#flask_admin.base.expose_plugview
|
||||
*/
|
||||
private class FlaskAdminExposePlugviewCall extends Flask::FlaskRouteSetup, DataFlow::CallCfgNode {
|
||||
FlaskAdminExposePlugviewCall() {
|
||||
this = API::moduleImport("flask_admin").getMember("expose_plugview").getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getUrlPatternArg() {
|
||||
result in [this.getArg(0), this.getArgByName("url")]
|
||||
}
|
||||
|
||||
override Parameter getARoutedParameter() {
|
||||
result = super.getARoutedParameter() and
|
||||
(
|
||||
exists(this.getUrlPattern())
|
||||
or
|
||||
// the first argument is `self`, and the second argument `cls` will receive the
|
||||
// containing flask_admin View class -- this is only relevant if the URL pattern
|
||||
// is not known
|
||||
not exists(this.getUrlPattern()) and
|
||||
not result = this.getARequestHandler().getArg([0, 1])
|
||||
)
|
||||
}
|
||||
|
||||
override Function getARequestHandler() {
|
||||
exists(Flask::FlaskViewClass cls |
|
||||
cls.getADecorator().getAFlowNode() = node and
|
||||
result = cls.getARequestHandler()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
108
python/ql/lib/semmle/python/frameworks/Pydantic.qll
Normal file
108
python/ql/lib/semmle/python/frameworks/Pydantic.qll
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `pydantic` PyPI package.
|
||||
*
|
||||
* See
|
||||
* - https://pypi.org/project/pydantic/
|
||||
* - https://pydantic-docs.helpmanual.io/
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Provides models for `pydantic` PyPI package.
|
||||
*
|
||||
* See
|
||||
* - https://pypi.org/project/pydantic/
|
||||
* - https://pydantic-docs.helpmanual.io/
|
||||
*/
|
||||
module Pydantic {
|
||||
/**
|
||||
* Provides models for `pydantic.BaseModel` subclasses (a pydantic model).
|
||||
*
|
||||
* See https://pydantic-docs.helpmanual.io/usage/models/.
|
||||
*/
|
||||
module BaseModel {
|
||||
/** Gets a reference to a `pydantic.BaseModel` subclass (a pydantic model). */
|
||||
API::Node subclassRef() {
|
||||
result = API::moduleImport("pydantic").getMember("BaseModel").getASubclass+()
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `pydantic.BaseModel` subclasses, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `BaseModel::instance()` to get references to instances of `pydantic.BaseModel`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** Gets a reference to an instance of a `pydantic.BaseModel` subclass. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
t.start() and
|
||||
instanceStepToPydanticModel(_, result)
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of a `pydantic.BaseModel` subclass. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* A step from an instance of a `pydantic.BaseModel` subclass, that might result in
|
||||
* an instance of a `pydantic.BaseModel` subclass.
|
||||
*
|
||||
* NOTE: We currently overapproximate, and treat all attributes as containing
|
||||
* another pydantic model. For the code below, we _could_ limit this to `main_foo`
|
||||
* and members of `other_foos`. IF THIS IS CHANGED, YOU MUST CHANGE THE ADDITIONAL
|
||||
* TAINT STEPS BELOW, SUCH THAT SIMPLE ACCESS OF SOMETHIGN LIKE `str` IS STILL
|
||||
* TAINTED.
|
||||
*
|
||||
*
|
||||
* ```py
|
||||
* class MyComplexModel(BaseModel):
|
||||
* field: str
|
||||
* main_foo: Foo
|
||||
* other_foos: List[Foo]
|
||||
* ```
|
||||
*/
|
||||
private predicate instanceStepToPydanticModel(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
// attributes (such as `model.foo`)
|
||||
nodeFrom = instance() and
|
||||
nodeTo.(DataFlow::AttrRead).getObject() = nodeFrom
|
||||
or
|
||||
// subscripts on attributes (such as `model.foo[0]`). This needs to handle nested
|
||||
// lists (such as `model.foo[0][0]`), and access being split into multiple
|
||||
// statements (such as `xs = model.foo; xs[0]`).
|
||||
//
|
||||
// To handle this we overapproximate which things are a Pydantic model, by
|
||||
// treating any subscript on anything that originates on a Pydantic model to also
|
||||
// be a Pydantic model. So `model[0]` will be an overapproximation, but should not
|
||||
// really cause problems (since we don't expect real code to contain such accesses)
|
||||
nodeFrom = instance() and
|
||||
nodeTo.asCfgNode().(SubscriptNode).getObject() = nodeFrom.asCfgNode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra taint propagation for `pydantic.BaseModel` subclasses. (note that these could also be `pydantic.BaseModel` subclasses)
|
||||
*/
|
||||
private class AdditionalTaintStep extends TaintTracking::AdditionalTaintStep {
|
||||
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
// NOTE: if `instanceStepToPydanticModel` is changed to be more precise, these
|
||||
// taint steps should be expanded, such that a field that has type `str` is
|
||||
// still tainted.
|
||||
instanceStepToPydanticModel(nodeFrom, nodeTo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
369
python/ql/lib/semmle/python/frameworks/RestFramework.qll
Normal file
369
python/ql/lib/semmle/python/frameworks/RestFramework.qll
Normal file
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `djangorestframework` PyPI package
|
||||
* (imported as `rest_framework`)
|
||||
*
|
||||
* See
|
||||
* - https://www.django-rest-framework.org/
|
||||
* - https://pypi.org/project/djangorestframework/
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
|
||||
private import semmle.python.frameworks.Django
|
||||
private import semmle.python.frameworks.Stdlib
|
||||
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Provides models for the `djangorestframework` PyPI package
|
||||
* (imported as `rest_framework`)
|
||||
*
|
||||
* See
|
||||
* - https://www.django-rest-framework.org/
|
||||
* - https://pypi.org/project/djangorestframework/
|
||||
*/
|
||||
private module RestFramework {
|
||||
// ---------------------------------------------------------------------------
|
||||
// rest_framework.views.APIView handling
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* An `API::Node` representing the `rest_framework.views.APIView` class or any subclass
|
||||
* that has explicitly been modeled in the CodeQL libraries.
|
||||
*/
|
||||
private class ModeledApiViewClasses extends Django::Views::View::ModeledSubclass {
|
||||
ModeledApiViewClasses() {
|
||||
this = API::moduleImport("rest_framework").getMember("views").getMember("APIView")
|
||||
or
|
||||
// imports generated by python/frameworks/internal/SubclassFinder.qll
|
||||
this =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("authtoken")
|
||||
.getMember("views")
|
||||
.getMember("APIView")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("authtoken")
|
||||
.getMember("views")
|
||||
.getMember("ObtainAuthToken")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("decorators").getMember("APIView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("generics").getMember("CreateAPIView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("generics").getMember("DestroyAPIView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("generics").getMember("GenericAPIView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("generics").getMember("ListAPIView")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework").getMember("generics").getMember("ListCreateAPIView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("generics").getMember("RetrieveAPIView")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("generics")
|
||||
.getMember("RetrieveDestroyAPIView")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework").getMember("generics").getMember("RetrieveUpdateAPIView")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("generics")
|
||||
.getMember("RetrieveUpdateDestroyAPIView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("generics").getMember("UpdateAPIView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("routers").getMember("APIRootView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("routers").getMember("SchemaView")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("schemas")
|
||||
.getMember("views")
|
||||
.getMember("APIView")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("schemas")
|
||||
.getMember("views")
|
||||
.getMember("SchemaView")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("viewsets").getMember("GenericViewSet")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("viewsets").getMember("ModelViewSet")
|
||||
or
|
||||
this =
|
||||
API::moduleImport("rest_framework").getMember("viewsets").getMember("ReadOnlyModelViewSet")
|
||||
or
|
||||
this = API::moduleImport("rest_framework").getMember("viewsets").getMember("ViewSet")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that has a super-type which is a rest_framework APIView class, therefore also
|
||||
* becoming a APIView class.
|
||||
*/
|
||||
class RestFrameworkApiViewClass extends PrivateDjango::DjangoViewClassFromSuperClass {
|
||||
RestFrameworkApiViewClass() {
|
||||
this.getABase() = any(ModeledApiViewClasses c).getASubclass*().getAUse().asExpr()
|
||||
}
|
||||
|
||||
override Function getARequestHandler() {
|
||||
result = super.getARequestHandler()
|
||||
or
|
||||
// TODO: This doesn't handle attribute assignment. Should be OK, but analysis is not as complete as with
|
||||
// points-to and `.lookup`, which would handle `post = my_post_handler` inside class def
|
||||
result = this.getAMethod() and
|
||||
result.getName() in [
|
||||
// these method names where found by looking through the APIView
|
||||
// implementation in
|
||||
// https://github.com/encode/django-rest-framework/blob/master/rest_framework/views.py#L104
|
||||
"initial", "http_method_not_allowed", "permission_denied", "throttled",
|
||||
"get_authenticate_header", "perform_content_negotiation", "perform_authentication",
|
||||
"check_permissions", "check_object_permissions", "check_throttles", "determine_version",
|
||||
"initialize_request", "finalize_response", "dispatch", "options"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// rest_framework.decorators.api_view handling
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* A function that is a request handler since it is decorated with `rest_framework.decorators.api_view`
|
||||
*/
|
||||
class RestFrameworkFunctionBasedView extends PrivateDjango::DjangoRouteHandler::Range {
|
||||
RestFrameworkFunctionBasedView() {
|
||||
this.getADecorator() =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("decorators")
|
||||
.getMember("api_view")
|
||||
.getACall()
|
||||
.asExpr()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensuring that all `RestFrameworkFunctionBasedView` are also marked as a
|
||||
* `HTTP::Server::RequestHandler`. We only need this for the ones that doesn't have a
|
||||
* known route setup.
|
||||
*/
|
||||
class RestFrameworkFunctionBasedViewWithoutKnownRoute extends HTTP::Server::RequestHandler::Range,
|
||||
PrivateDjango::DjangoRouteHandler instanceof RestFrameworkFunctionBasedView {
|
||||
RestFrameworkFunctionBasedViewWithoutKnownRoute() {
|
||||
not exists(PrivateDjango::DjangoRouteSetup setup | setup.getARequestHandler() = this)
|
||||
}
|
||||
|
||||
override Parameter getARoutedParameter() {
|
||||
// Since we don't know the URL pattern, we simply mark all parameters as a routed
|
||||
// parameter. This should give us more RemoteFlowSources but could also lead to
|
||||
// more FPs. If this turns out to be the wrong tradeoff, we can always change our mind.
|
||||
result in [this.getArg(_), this.getArgByName(_)] and
|
||||
not result = any(int i | i < this.getFirstPossibleRoutedParamIndex() | this.getArg(i))
|
||||
}
|
||||
|
||||
override string getFramework() { result = "Django (rest_framework)" }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// request modeling
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* A parameter that will receive a `rest_framework.request.Request` instance when a
|
||||
* request handler is invoked.
|
||||
*/
|
||||
private class RestFrameworkRequestHandlerRequestParam extends Request::InstanceSource,
|
||||
RemoteFlowSource::Range, DataFlow::ParameterNode {
|
||||
RestFrameworkRequestHandlerRequestParam() {
|
||||
// rest_framework.views.APIView subclass
|
||||
exists(RestFrameworkApiViewClass vc |
|
||||
this.getParameter() =
|
||||
vc.getARequestHandler().(PrivateDjango::DjangoRouteHandler).getRequestParam()
|
||||
)
|
||||
or
|
||||
// annotated with @api_view decorator
|
||||
exists(PrivateDjango::DjangoRouteHandler rh | rh instanceof RestFrameworkFunctionBasedView |
|
||||
this.getParameter() = rh.getRequestParam()
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = "rest_framework.request.HttpRequest" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `rest_framework.request.Request` class
|
||||
*
|
||||
* See https://www.django-rest-framework.org/api-guide/requests/.
|
||||
*/
|
||||
module Request {
|
||||
/** Gets a reference to the `rest_framework.request.Request` class. */
|
||||
private API::Node classRef() {
|
||||
result = API::moduleImport("rest_framework").getMember("request").getMember("Request")
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `rest_framework.request.Request`, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `Request::instance()` to get references to instances of `rest_framework.request.Request`.
|
||||
*/
|
||||
abstract class InstanceSource extends PrivateDjango::django::http::request::HttpRequest::InstanceSource {
|
||||
}
|
||||
|
||||
/** A direct instantiation of `rest_framework.request.Request`. */
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `rest_framework.request.Request`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `rest_framework.request.Request`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* Taint propagation for `rest_framework.request.Request`.
|
||||
*/
|
||||
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
|
||||
InstanceTaintSteps() { this = "rest_framework.request.Request" }
|
||||
|
||||
override DataFlow::Node getInstance() { result = instance() }
|
||||
|
||||
override string getAttributeName() {
|
||||
result in ["data", "query_params", "user", "auth", "content_type", "stream"]
|
||||
}
|
||||
|
||||
override string getMethodName() { none() }
|
||||
|
||||
override string getAsyncMethodName() { none() }
|
||||
}
|
||||
|
||||
/** An attribute read that is a `MultiValueDict` instance. */
|
||||
private class MultiValueDictInstances extends Django::MultiValueDict::InstanceSource {
|
||||
MultiValueDictInstances() {
|
||||
this.(DataFlow::AttrRead).getObject() = instance() and
|
||||
this.(DataFlow::AttrRead).getAttributeName() = "query_params"
|
||||
}
|
||||
}
|
||||
|
||||
/** An attribute read that is a `User` instance. */
|
||||
private class UserInstances extends Django::User::InstanceSource {
|
||||
UserInstances() {
|
||||
this.(DataFlow::AttrRead).getObject() = instance() and
|
||||
this.(DataFlow::AttrRead).getAttributeName() = "user"
|
||||
}
|
||||
}
|
||||
|
||||
/** An attribute read that is a file-like instance. */
|
||||
private class FileLikeInstances extends Stdlib::FileLikeObject::InstanceSource {
|
||||
FileLikeInstances() {
|
||||
this.(DataFlow::AttrRead).getObject() = instance() and
|
||||
this.(DataFlow::AttrRead).getAttributeName() = "stream"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// response modeling
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* Provides models for the `rest_framework.response.Response` class
|
||||
*
|
||||
* See https://www.django-rest-framework.org/api-guide/responses/.
|
||||
*/
|
||||
module Response {
|
||||
/** Gets a reference to the `rest_framework.response.Response` class. */
|
||||
private API::Node classRef() {
|
||||
result = API::moduleImport("rest_framework").getMember("response").getMember("Response")
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `rest_framework.response.Response`, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `Response::instance()` to get references to instances of `rest_framework.response.Response`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** A direct instantiation of `rest_framework.response.Response`. */
|
||||
private class ClassInstantiation extends PrivateDjango::django::http::response::HttpResponse::InstanceSource,
|
||||
DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
|
||||
override DataFlow::Node getBody() { result in [this.getArg(0), this.getArgByName("data")] }
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() {
|
||||
result in [this.getArg(5), this.getArgByName("content_type")]
|
||||
}
|
||||
|
||||
override string getMimetypeDefault() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exception response modeling
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* Provides models for the `rest_framework.exceptions.APIException` class and subclasses
|
||||
*
|
||||
* See https://www.django-rest-framework.org/api-guide/exceptions/#api-reference
|
||||
*/
|
||||
module APIException {
|
||||
/** A direct instantiation of `rest_framework.exceptions.APIException` or subclass. */
|
||||
private class ClassInstantiation extends HTTP::Server::HttpResponse::Range,
|
||||
DataFlow::CallCfgNode {
|
||||
string className;
|
||||
|
||||
ClassInstantiation() {
|
||||
className in [
|
||||
"APIException", "ValidationError", "ParseError", "AuthenticationFailed",
|
||||
"NotAuthenticated", "PermissionDenied", "NotFound", "MethodNotAllowed", "NotAcceptable",
|
||||
"UnsupportedMediaType", "Throttled"
|
||||
] and
|
||||
this =
|
||||
API::moduleImport("rest_framework")
|
||||
.getMember("exceptions")
|
||||
.getMember(className)
|
||||
.getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() {
|
||||
className in [
|
||||
"APIException", "ValidationError", "ParseError", "AuthenticationFailed",
|
||||
"NotAuthenticated", "PermissionDenied", "NotFound", "NotAcceptable"
|
||||
] and
|
||||
result = this.getArg(0)
|
||||
or
|
||||
className in ["MethodNotAllowed", "UnsupportedMediaType", "Throttled"] and
|
||||
result = this.getArg(1)
|
||||
or
|
||||
result = this.getArgByName("detail")
|
||||
}
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
|
||||
|
||||
override string getMimetypeDefault() { none() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,9 +313,9 @@ module SqlAlchemy {
|
||||
* A construction of a `sqlalchemy.sql.expression.TextClause`, which represents a
|
||||
* textual SQL string directly.
|
||||
*/
|
||||
abstract class TextClauseConstruction extends DataFlow::CallCfgNode {
|
||||
abstract class TextClauseConstruction extends SqlConstruction::Range, DataFlow::CallCfgNode {
|
||||
/** Gets the argument that specifies the SQL text. */
|
||||
DataFlow::Node getTextArg() { result in [this.getArg(0), this.getArgByName("text")] }
|
||||
override DataFlow::Node getSql() { result in [this.getArg(0), this.getArgByName("text")] }
|
||||
}
|
||||
|
||||
/** `TextClause` constructions from the `sqlalchemy` package. */
|
||||
|
||||
162
python/ql/lib/semmle/python/frameworks/Starlette.qll
Normal file
162
python/ql/lib/semmle/python/frameworks/Starlette.qll
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `starlette` PyPI package.
|
||||
*
|
||||
* See
|
||||
* - https://pypi.org/project/starlette/
|
||||
* - https://www.starlette.io/
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
|
||||
private import semmle.python.frameworks.Stdlib
|
||||
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Provides models for `starlette` PyPI package.
|
||||
*
|
||||
* See
|
||||
* - https://pypi.org/project/starlette/
|
||||
* - https://www.starlette.io/
|
||||
*/
|
||||
module Starlette {
|
||||
/**
|
||||
* Provides models for the `starlette.websockets.WebSocket` class
|
||||
*
|
||||
* See https://www.starlette.io/websockets/.
|
||||
*/
|
||||
module WebSocket {
|
||||
/** Gets a reference to the `starlette.websockets.WebSocket` class. */
|
||||
API::Node classRef() {
|
||||
result = API::moduleImport("starlette").getMember("websockets").getMember("WebSocket")
|
||||
or
|
||||
result = API::moduleImport("fastapi").getMember("WebSocket")
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `starlette.websockets.WebSocket`, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `WebSocket::instance()` to get references to instances of `starlette.websockets.WebSocket`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** A direct instantiation of `starlette.websockets.WebSocket`. */
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `starlette.websockets.WebSocket`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `starlette.websockets.WebSocket`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* Taint propagation for `starlette.websockets.WebSocket`.
|
||||
*/
|
||||
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
|
||||
InstanceTaintSteps() { this = "starlette.websockets.WebSocket" }
|
||||
|
||||
override DataFlow::Node getInstance() { result = instance() }
|
||||
|
||||
override string getAttributeName() { result in ["url", "headers", "query_params", "cookies"] }
|
||||
|
||||
override string getMethodName() { none() }
|
||||
|
||||
override string getAsyncMethodName() {
|
||||
result in [
|
||||
"receive", "receive_bytes", "receive_text", "receive_json", "iter_bytes", "iter_text",
|
||||
"iter_json"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/** An attribute read on a `starlette.websockets.WebSocket` instance that is a `starlette.requests.URL` instance. */
|
||||
private class UrlInstances extends URL::InstanceSource {
|
||||
UrlInstances() {
|
||||
this.(DataFlow::AttrRead).getObject() = instance() and
|
||||
this.(DataFlow::AttrRead).getAttributeName() = "url"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `starlette.requests.URL` class
|
||||
*
|
||||
* See the URL part of https://www.starlette.io/websockets/.
|
||||
*/
|
||||
module URL {
|
||||
/** Gets a reference to the `starlette.requests.URL` class. */
|
||||
private API::Node classRef() {
|
||||
result = API::moduleImport("starlette").getMember("requests").getMember("URL")
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `starlette.requests.URL`, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `URL::instance()` to get references to instances of `starlette.requests.URL`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** A direct instantiation of `starlette.requests.URL`. */
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `starlette.requests.URL`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `starlette.requests.URL`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* Taint propagation for `starlette.requests.URL`.
|
||||
*/
|
||||
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
|
||||
InstanceTaintSteps() { this = "starlette.requests.URL" }
|
||||
|
||||
override DataFlow::Node getInstance() { result = instance() }
|
||||
|
||||
override string getAttributeName() {
|
||||
result in [
|
||||
"components", "netloc", "path", "query", "fragment", "username", "password", "hostname",
|
||||
"port"
|
||||
]
|
||||
}
|
||||
|
||||
override string getMethodName() { none() }
|
||||
|
||||
override string getAsyncMethodName() { none() }
|
||||
}
|
||||
|
||||
/** An attribute read on a `starlette.requests.URL` instance that is a `urllib.parse.SplitResult` instance. */
|
||||
private class UrlSplitInstances extends Stdlib::SplitResult::InstanceSource instanceof DataFlow::AttrRead {
|
||||
UrlSplitInstances() {
|
||||
super.getObject() = instance() and
|
||||
super.getAttributeName() = "components"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,74 @@ module Stdlib {
|
||||
override string getAsyncMethodName() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `urllib.parse.SplitResult` class
|
||||
*
|
||||
* See https://docs.python.org/3.9/library/urllib.parse.html#urllib.parse.SplitResult.
|
||||
*/
|
||||
module SplitResult {
|
||||
/** Gets a reference to the `urllib.parse.SplitResult` class. */
|
||||
private API::Node classRef() {
|
||||
result = API::moduleImport("urllib").getMember("parse").getMember("SplitResult")
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `urllib.parse.SplitResult`, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `SplitResult::instance()` to get references to instances of `urllib.parse.SplitResult`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** A direct instantiation of `urllib.parse.SplitResult`. */
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `urllib.parse.SplitResult`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `urllib.parse.SplitResult`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* Taint propagation for `urllib.parse.SplitResult`.
|
||||
*/
|
||||
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
|
||||
InstanceTaintSteps() { this = "urllib.parse.SplitResult" }
|
||||
|
||||
override DataFlow::Node getInstance() { result = instance() }
|
||||
|
||||
override string getAttributeName() {
|
||||
result in [
|
||||
"netloc", "path", "query", "fragment", "username", "password", "hostname", "port"
|
||||
]
|
||||
}
|
||||
|
||||
override string getMethodName() { none() }
|
||||
|
||||
override string getAsyncMethodName() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra taint propagation for `urllib.parse.SplitResult`, not covered by `InstanceTaintSteps`.
|
||||
*/
|
||||
private class AdditionalTaintStep extends TaintTracking::AdditionalTaintStep {
|
||||
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
// TODO
|
||||
none()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -784,7 +852,7 @@ private module StdlibPrivate {
|
||||
Base64EncodeCall() {
|
||||
name in [
|
||||
"b64encode", "standard_b64encode", "urlsafe_b64encode", "b32encode", "b16encode",
|
||||
"encodestring", "a85encode", "b85encode", "encodebytes"
|
||||
"encodestring", "a85encode", "b85encode", "encodebytes", "b32hexencode"
|
||||
] and
|
||||
this = base64().getMember(name).getACall()
|
||||
}
|
||||
@@ -799,7 +867,7 @@ private module StdlibPrivate {
|
||||
] and
|
||||
result = "Base64"
|
||||
or
|
||||
name = "b32encode" and result = "Base32"
|
||||
name in ["b32encode", "b32hexencode"] and result = "Base32"
|
||||
or
|
||||
name = "b16encode" and result = "Base16"
|
||||
or
|
||||
@@ -816,7 +884,7 @@ private module StdlibPrivate {
|
||||
Base64DecodeCall() {
|
||||
name in [
|
||||
"b64decode", "standard_b64decode", "urlsafe_b64decode", "b32decode", "b16decode",
|
||||
"decodestring", "a85decode", "b85decode", "decodebytes"
|
||||
"decodestring", "a85decode", "b85decode", "decodebytes", "b32hexdecode"
|
||||
] and
|
||||
this = base64().getMember(name).getACall()
|
||||
}
|
||||
@@ -833,7 +901,7 @@ private module StdlibPrivate {
|
||||
] and
|
||||
result = "Base64"
|
||||
or
|
||||
name = "b32decode" and result = "Base32"
|
||||
name in ["b32decode", "b32hexdecode"] and result = "Base32"
|
||||
or
|
||||
name = "b16decode" and result = "Base16"
|
||||
or
|
||||
@@ -1384,7 +1452,7 @@ private module StdlibPrivate {
|
||||
"is_symlink", "is_socket", "is_fifo", "is_block_device", "is_char_device", "iter_dir",
|
||||
"lchmod", "lstat", "mkdir", "open", "owner", "read_bytes", "read_text", "readlink",
|
||||
"rename", "replace", "resolve", "rglob", "rmdir", "samefile", "symlink_to", "touch",
|
||||
"unlink", "link_to", "write_bytes", "write_text"
|
||||
"unlink", "link_to", "write_bytes", "write_text", "hardlink_to"
|
||||
] and
|
||||
pathlibPath().flowsTo(fileAccess.getObject()) and
|
||||
fileAccess.(DataFlow::LocalSourceNode).flowsTo(this.getFunction())
|
||||
@@ -1466,15 +1534,36 @@ private module StdlibPrivate {
|
||||
// ---------------------------------------------------------------------------
|
||||
// hashlib
|
||||
// ---------------------------------------------------------------------------
|
||||
/** Gets a back-reference to the hashname argument `arg` that was used in a call to `hashlib.new`. */
|
||||
private DataFlow::TypeTrackingNode hashlibNewCallNameBacktracker(
|
||||
DataFlow::TypeBackTracker t, DataFlow::Node arg
|
||||
) {
|
||||
t.start() and
|
||||
hashlibNewCallImpl(_, arg) and
|
||||
result = arg.getALocalSource()
|
||||
or
|
||||
exists(DataFlow::TypeBackTracker t2 |
|
||||
result = hashlibNewCallNameBacktracker(t2, arg).backtrack(t2, t)
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets a back-reference to the hashname argument `arg` that was used in a call to `hashlib.new`. */
|
||||
private DataFlow::LocalSourceNode hashlibNewCallNameBacktracker(DataFlow::Node arg) {
|
||||
result = hashlibNewCallNameBacktracker(DataFlow::TypeBackTracker::end(), arg)
|
||||
}
|
||||
|
||||
/** Holds when `call` is a call to `hashlib.new` with `nameArg` as the first argument. */
|
||||
private predicate hashlibNewCallImpl(DataFlow::CallCfgNode call, DataFlow::Node nameArg) {
|
||||
call = API::moduleImport("hashlib").getMember("new").getACall() and
|
||||
nameArg in [call.getArg(0), call.getArgByName("name")]
|
||||
}
|
||||
|
||||
/** Gets a call to `hashlib.new` with `algorithmName` as the first argument. */
|
||||
private DataFlow::CallCfgNode hashlibNewCall(string algorithmName) {
|
||||
exists(DataFlow::Node nameArg |
|
||||
result = API::moduleImport("hashlib").getMember("new").getACall() and
|
||||
nameArg in [result.getArg(0), result.getArgByName("name")] and
|
||||
exists(StrConst str |
|
||||
nameArg.getALocalSource() = DataFlow::exprNode(str) and
|
||||
algorithmName = str.getText()
|
||||
)
|
||||
exists(DataFlow::Node origin, DataFlow::Node nameArg |
|
||||
origin = hashlibNewCallNameBacktracker(nameArg) and
|
||||
algorithmName = origin.asExpr().(StrConst).getText() and
|
||||
hashlibNewCallImpl(result, nameArg)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1749,6 +1838,30 @@ private module StdlibPrivate {
|
||||
|
||||
override string getKind() { result = Escaping::getRegexKind() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// urllib
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* A call to `urllib.parse.urlsplit`
|
||||
*
|
||||
* See https://docs.python.org/3.9/library/urllib.parse.html#urllib.parse.urlsplit
|
||||
*/
|
||||
class UrllibParseUrlsplitCall extends Stdlib::SplitResult::InstanceSource, DataFlow::CallCfgNode {
|
||||
UrllibParseUrlsplitCall() {
|
||||
this = API::moduleImport("urllib").getMember("parse").getMember("urlsplit").getACall()
|
||||
}
|
||||
|
||||
/** Gets the argument that specifies the URL. */
|
||||
DataFlow::Node getUrl() { result in [this.getArg(0), this.getArgByName("url")] }
|
||||
}
|
||||
|
||||
/** Extra taint-step such that the result of `urllib.parse.urlsplit(tainted_string)` is tainted. */
|
||||
private class UrllibParseUrlsplitCallAdditionalTaintStep extends TaintTracking::AdditionalTaintStep {
|
||||
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
nodeTo.(UrllibParseUrlsplitCall).getUrl() = nodeFrom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
101
python/ql/lib/semmle/python/frameworks/Toml.qll
Normal file
101
python/ql/lib/semmle/python/frameworks/Toml.qll
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `toml` PyPI package.
|
||||
*
|
||||
* See
|
||||
* - https://pypi.org/project/toml/
|
||||
* - https://github.com/uiri/toml#api-reference
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `toml` PyPI package
|
||||
*
|
||||
* See
|
||||
* - https://pypi.org/project/toml/
|
||||
* - https://github.com/uiri/toml#api-reference
|
||||
*/
|
||||
private module Toml {
|
||||
/**
|
||||
* A call to `toml.loads`
|
||||
*
|
||||
* See https://github.com/uiri/toml#api-reference
|
||||
*/
|
||||
private class TomlLoadsCall extends Decoding::Range, DataFlow::CallCfgNode {
|
||||
TomlLoadsCall() {
|
||||
this = API::moduleImport("toml").getMember("loads").getACall()
|
||||
or
|
||||
this = API::moduleImport("toml").getMember("decoder").getMember("loads").getACall()
|
||||
}
|
||||
|
||||
override predicate mayExecuteInput() { none() }
|
||||
|
||||
override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("s")] }
|
||||
|
||||
override DataFlow::Node getOutput() { result = this }
|
||||
|
||||
override string getFormat() { result = "TOML" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `toml.load`
|
||||
*
|
||||
* See https://github.com/uiri/toml#api-reference
|
||||
*/
|
||||
private class TomlLoadCall extends Decoding::Range, DataFlow::CallCfgNode {
|
||||
TomlLoadCall() {
|
||||
this = API::moduleImport("toml").getMember("load").getACall()
|
||||
or
|
||||
this = API::moduleImport("toml").getMember("decoder").getMember("load").getACall()
|
||||
}
|
||||
|
||||
override predicate mayExecuteInput() { none() }
|
||||
|
||||
override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("f")] }
|
||||
|
||||
override DataFlow::Node getOutput() { result = this }
|
||||
|
||||
override string getFormat() { result = "TOML" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `toml.dumps`
|
||||
*
|
||||
* See https://github.com/uiri/toml#api-reference
|
||||
*/
|
||||
private class TomlDumpsCall extends Encoding::Range, DataFlow::CallCfgNode {
|
||||
TomlDumpsCall() {
|
||||
this = API::moduleImport("toml").getMember("dumps").getACall()
|
||||
or
|
||||
this = API::moduleImport("toml").getMember("encoder").getMember("dumps").getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("o")] }
|
||||
|
||||
override DataFlow::Node getOutput() { result = this }
|
||||
|
||||
override string getFormat() { result = "TOML" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `toml.dump`
|
||||
*
|
||||
* See https://github.com/uiri/toml#api-reference
|
||||
*/
|
||||
private class TomlDumpCall extends Encoding::Range, DataFlow::CallCfgNode {
|
||||
TomlDumpCall() {
|
||||
this = API::moduleImport("toml").getMember("dump").getACall()
|
||||
or
|
||||
this = API::moduleImport("toml").getMember("encoder").getMember("dump").getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("o")] }
|
||||
|
||||
override DataFlow::Node getOutput() { result in [this.getArg(1), this.getArgByName("f")] }
|
||||
|
||||
override string getFormat() { result = "TOML" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Has predicates to help find subclasses in library code. Should only be used to aid in
|
||||
* the manual library modeling process,
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.filters.Tests
|
||||
|
||||
// very much inspired by the draft at https://github.com/github/codeql/pull/5632
|
||||
private module NotExposed {
|
||||
// Instructions:
|
||||
// This needs to be automated better, but for this prototype, here are some rough instructions:
|
||||
// 0) get a database of the library you are about to model
|
||||
// 1) fill out the `getAlreadyModeledClass` body below
|
||||
// 2) quick-eval the `quickEvalMe` predicate below, and copy the output to your modeling predicate
|
||||
class MySpec extends FindSubclassesSpec {
|
||||
MySpec() { this = "MySpec" }
|
||||
|
||||
override API::Node getAlreadyModeledClass() {
|
||||
// FILL ME OUT ! (but don't commit with any changes)
|
||||
none()
|
||||
// for example
|
||||
// result = API::moduleImport("rest_framework").getMember("views").getMember("APIView")
|
||||
}
|
||||
}
|
||||
|
||||
predicate quickEvalMe(string newImport) {
|
||||
newImport =
|
||||
"// imports generated by python/frameworks/internal/SubclassFinder.qll\n" + "this = API::" +
|
||||
concat(string newModelFullyQualified |
|
||||
newModel(any(MySpec spec), newModelFullyQualified, _, _, _)
|
||||
|
|
||||
fullyQualifiedToAPIGraphPath(newModelFullyQualified), " or this = API::"
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Implementation below
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// We are looking to find all subclassed of the already modelled classes, and ideally
|
||||
// we would identify an `API::Node` for each (then `toString` would give the API
|
||||
// path).
|
||||
//
|
||||
// An inherent problem with API graphs is that there doesn't need to exist a result
|
||||
// for the API graph path that we want to add to our modeling (the path to the new
|
||||
// subclass). As an example, the following query has no results when evaluated against
|
||||
// a django/django DB.
|
||||
//
|
||||
// select API::moduleImport("django") .getMember("contrib") .getMember("admin")
|
||||
// .getMember("views") .getMember("main") .getMember("ChangeListSearchForm")
|
||||
//
|
||||
//
|
||||
// Since it is a Form subclass that we would want to capture for our Django modeling,
|
||||
// we want to extend our modeling (that is written in a qll file) with exactly that
|
||||
// piece of code, but since the API::Node doesn't exist, we can't select that from a
|
||||
// predicate and print its path. We need a different approach, and for that we use
|
||||
// fully qualified names to capture new classes/new aliases, and transform these into
|
||||
// API paths (to be included in the modeling that is inserted into the `.qll` files),
|
||||
// see `fullyQualifiedToAPIGraphPath`.
|
||||
//
|
||||
// NOTE: this implementation was originally created to help with automatically
|
||||
// modeling packages in mind, and has been adjusted to help with manual library
|
||||
// modeling. See https://github.com/github/codeql/pull/5632 for more discussion.
|
||||
//
|
||||
//
|
||||
bindingset[fullyQaulified]
|
||||
string fullyQualifiedToAPIGraphPath(string fullyQaulified) {
|
||||
result = "moduleImport(\"" + fullyQaulified.replaceAll(".", "\").getMember(\"") + "\")"
|
||||
}
|
||||
|
||||
bindingset[this]
|
||||
abstract class FindSubclassesSpec extends string {
|
||||
abstract API::Node getAlreadyModeledClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `newModelFullyQualified` describes either a new subclass, or a new alias, belonging to `spec` that we should include in our automated modeling.
|
||||
* This new element is defined by `ast`, which is defined at `loc` in the module `mod`.
|
||||
*/
|
||||
query predicate newModel(
|
||||
FindSubclassesSpec spec, string newModelFullyQualified, AstNode ast, Module mod, Location loc
|
||||
) {
|
||||
(
|
||||
newSubclass(spec, newModelFullyQualified, ast, mod, loc)
|
||||
or
|
||||
newDirectAlias(spec, newModelFullyQualified, ast, mod, loc)
|
||||
or
|
||||
newImportStar(spec, newModelFullyQualified, ast, mod, _, _, loc)
|
||||
)
|
||||
}
|
||||
|
||||
API::Node newOrExistingModeling(FindSubclassesSpec spec) {
|
||||
result = spec.getAlreadyModeledClass()
|
||||
or
|
||||
exists(string newSubclassName |
|
||||
newModel(spec, newSubclassName, _, _, _) and
|
||||
result.getPath() = fullyQualifiedToAPIGraphPath(newSubclassName)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[fullyQualifiedName]
|
||||
predicate alreadyModeled(FindSubclassesSpec spec, string fullyQualifiedName) {
|
||||
fullyQualifiedToAPIGraphPath(fullyQualifiedName) = spec.getAlreadyModeledClass().getPath()
|
||||
}
|
||||
|
||||
predicate isNonTestProjectCode(AstNode ast) {
|
||||
not ast.getScope*() instanceof TestScope and
|
||||
not ast.getLocation().getFile().getRelativePath().matches("tests/%") and
|
||||
exists(ast.getLocation().getFile().getRelativePath())
|
||||
}
|
||||
|
||||
predicate hasAllStatement(Module mod) {
|
||||
exists(AssignStmt a, GlobalVariable all |
|
||||
a.defines(all) and
|
||||
a.getScope() = mod and
|
||||
all.getId() = "__all__"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `newAliasFullyQualified` describes new alias originating from the import
|
||||
* `from <module> import <member> [as <new-name>]`, where `<module>.<member>` belongs to
|
||||
* `spec`.
|
||||
* So if this import happened in module `foo.bar`, `newAliasFullyQualified` would be
|
||||
* `foo.bar.<member>` (or `foo.bar.<new-name>`).
|
||||
*
|
||||
* Note that this predicate currently respects `__all__` in sort of a backwards fashion.
|
||||
* - if `__all__` is defined in module `foo.bar`, we only allow new aliases where the member name is also in `__all__`. (this doesn't map 100% to the semantics of imports though)
|
||||
* - If `__all__` is not defined we don't impose any limitations.
|
||||
*
|
||||
* Also note that we don't currently consider deleting module-attributes at all, so in the code snippet below, we would consider that `my_module.foo` is a
|
||||
* reference to `django.foo`, although `my_module.foo` isn't even available at runtime. (there currently also isn't any code to discover that `my_module.bar`
|
||||
* is an alias to `django.foo`)
|
||||
* ```py
|
||||
* # module my_module
|
||||
* from django import foo
|
||||
* bar = foo
|
||||
* del foo
|
||||
* ```
|
||||
*/
|
||||
predicate newDirectAlias(
|
||||
FindSubclassesSpec spec, string newAliasFullyQualified, ImportMember importMember, Module mod,
|
||||
Location loc
|
||||
) {
|
||||
importMember = newOrExistingModeling(spec).getAUse().asExpr() and
|
||||
importMember.getScope() = mod and
|
||||
loc = importMember.getLocation() and
|
||||
(
|
||||
mod.isPackageInit() and
|
||||
newAliasFullyQualified = mod.getPackageName() + "." + importMember.getName()
|
||||
or
|
||||
not mod.isPackageInit() and
|
||||
newAliasFullyQualified = mod.getName() + "." + importMember.getName()
|
||||
) and
|
||||
(
|
||||
not hasAllStatement(mod)
|
||||
or
|
||||
mod.declaredInAll(importMember.getName())
|
||||
) and
|
||||
not alreadyModeled(spec, newAliasFullyQualified) and
|
||||
isNonTestProjectCode(importMember)
|
||||
}
|
||||
|
||||
/** same as `newDirectAlias` predicate, but handling `from <module> import *`, considering all `<member>`, where `<module>.<member>` belongs to `spec`. */
|
||||
predicate newImportStar(
|
||||
FindSubclassesSpec spec, string newAliasFullyQualified, ImportStar importStar, Module mod,
|
||||
API::Node relevantClass, string relevantName, Location loc
|
||||
) {
|
||||
relevantClass = newOrExistingModeling(spec) and
|
||||
loc = importStar.getLocation() and
|
||||
importStar.getScope() = mod and
|
||||
// WHAT A HACK :D :D
|
||||
relevantClass.getPath() =
|
||||
relevantClass.getAPredecessor().getPath() + ".getMember(\"" + relevantName + "\")" and
|
||||
relevantClass.getAPredecessor().getAUse().asExpr() = importStar.getModule() and
|
||||
(
|
||||
mod.isPackageInit() and
|
||||
newAliasFullyQualified = mod.getPackageName() + "." + relevantName
|
||||
or
|
||||
not mod.isPackageInit() and
|
||||
newAliasFullyQualified = mod.getName() + "." + relevantName
|
||||
) and
|
||||
(
|
||||
not hasAllStatement(mod)
|
||||
or
|
||||
mod.declaredInAll(relevantName)
|
||||
) and
|
||||
not alreadyModeled(spec, newAliasFullyQualified) and
|
||||
isNonTestProjectCode(importStar)
|
||||
}
|
||||
|
||||
/** Holds if `classExpr` defines a new subclass that belongs to `spec`, which has the fully qualified name `newSubclassQualified`. */
|
||||
predicate newSubclass(
|
||||
FindSubclassesSpec spec, string newSubclassQualified, ClassExpr classExpr, Module mod,
|
||||
Location loc
|
||||
) {
|
||||
classExpr = newOrExistingModeling(spec).getASubclass*().getAUse().asExpr() and
|
||||
classExpr.getScope() = mod and
|
||||
newSubclassQualified = mod.getName() + "." + classExpr.getName() and
|
||||
loc = classExpr.getLocation() and
|
||||
not alreadyModeled(spec, newSubclassQualified) and
|
||||
isNonTestProjectCode(classExpr)
|
||||
}
|
||||
}
|
||||
47
python/ql/lib/semmle/python/internal/Awaited.qll
Normal file
47
python/ql/lib/semmle/python/internal/Awaited.qll
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Provides helper class for defining additional API graph edges.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Holds if `result` is the result of awaiting `awaitedValue`.
|
||||
*/
|
||||
cached
|
||||
DataFlow::Node awaited(DataFlow::Node awaitedValue) {
|
||||
// `await` x
|
||||
// - `awaitedValue` is `x`
|
||||
// - `result` is `await x`
|
||||
exists(Await await |
|
||||
await.getValue() = awaitedValue.asExpr() and
|
||||
result.asExpr() = await
|
||||
)
|
||||
or
|
||||
// `async for x in l`
|
||||
// - `awaitedValue` is `l`
|
||||
// - `result` is `l` (`x` is behind a read step)
|
||||
exists(AsyncFor asyncFor |
|
||||
// To consider `x` the result of awaiting, we would use asyncFor.getTarget() = awaitedValue.asExpr(),
|
||||
// but that is behind a read step rather than a flow step.
|
||||
asyncFor.getIter() = awaitedValue.asExpr() and
|
||||
result.asExpr() = asyncFor.getIter()
|
||||
)
|
||||
or
|
||||
// `async with x as y`
|
||||
// - `awaitedValue` is `x`
|
||||
// - `result` is `x` and `y` if it exists
|
||||
exists(AsyncWith asyncWith |
|
||||
awaitedValue.asExpr() = asyncWith.getContextExpr() and
|
||||
result.asExpr() in [
|
||||
// `x`
|
||||
asyncWith.getContextExpr(),
|
||||
// `y`, if it exists
|
||||
asyncWith.getOptionalVars()
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -248,7 +248,7 @@ class UnicodeObjectInternal extends ConstantObjectInternal, TUnicode {
|
||||
override ObjectInternal getClass() { result = TBuiltinClassObject(Builtin::special("unicode")) }
|
||||
|
||||
override Builtin getBuiltin() {
|
||||
result.(Builtin).strValue() = this.strValue() and
|
||||
result.strValue() = this.strValue() and
|
||||
result.getClass() = Builtin::special("unicode")
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ class BytesObjectInternal extends ConstantObjectInternal, TBytes {
|
||||
override ObjectInternal getClass() { result = TBuiltinClassObject(Builtin::special("bytes")) }
|
||||
|
||||
override Builtin getBuiltin() {
|
||||
result.(Builtin).strValue() = this.strValue() and
|
||||
result.strValue() = this.strValue() and
|
||||
result.getClass() = Builtin::special("bytes")
|
||||
}
|
||||
|
||||
|
||||
@@ -147,9 +147,7 @@ class Value extends TObject {
|
||||
* Class representing modules in the Python program
|
||||
* Each `ModuleValue` represents a module object in the Python program.
|
||||
*/
|
||||
class ModuleValue extends Value {
|
||||
ModuleValue() { this instanceof ModuleObjectInternal }
|
||||
|
||||
class ModuleValue extends Value instanceof ModuleObjectInternal {
|
||||
/**
|
||||
* Holds if this module "exports" name.
|
||||
* That is, does it define `name` in `__all__` or is
|
||||
@@ -159,7 +157,7 @@ class ModuleValue extends Value {
|
||||
predicate exports(string name) { PointsTo::moduleExports(this, name) }
|
||||
|
||||
/** Gets the scope for this module, provided that it is a Python module. */
|
||||
ModuleScope getScope() { result = this.(ModuleObjectInternal).getSourceModule() }
|
||||
ModuleScope getScope() { result = super.getSourceModule() }
|
||||
|
||||
/**
|
||||
* Gets the container path for this module. Will be the file for a Python module,
|
||||
@@ -181,7 +179,7 @@ class ModuleValue extends Value {
|
||||
predicate isPackage() { this instanceof PackageObjectInternal }
|
||||
|
||||
/** Whether the complete set of names "exported" by this module can be accurately determined */
|
||||
predicate hasCompleteExportInfo() { this.(ModuleObjectInternal).hasCompleteExportInfo() }
|
||||
predicate hasCompleteExportInfo() { super.hasCompleteExportInfo() }
|
||||
|
||||
/** Get a module that this module imports */
|
||||
ModuleValue getAnImportedModule() { result.importedAs(this.getScope().getAnImportedModuleName()) }
|
||||
@@ -452,23 +450,21 @@ class CallableValue extends Value {
|
||||
* Class representing bound-methods, such as `o.func`, where `o` is an instance
|
||||
* of a class that has a callable attribute `func`.
|
||||
*/
|
||||
class BoundMethodValue extends CallableValue {
|
||||
BoundMethodValue() { this instanceof BoundMethodObjectInternal }
|
||||
|
||||
class BoundMethodValue extends CallableValue instanceof BoundMethodObjectInternal {
|
||||
/**
|
||||
* Gets the callable that will be used when `this` is called.
|
||||
* The actual callable for `func` in `o.func`.
|
||||
*/
|
||||
CallableValue getFunction() { result = this.(BoundMethodObjectInternal).getFunction() }
|
||||
CallableValue getFunction() { result = super.getFunction() }
|
||||
|
||||
/**
|
||||
* Gets the value that will be used for the `self` parameter when `this` is called.
|
||||
* The value for `o` in `o.func`.
|
||||
*/
|
||||
Value getSelf() { result = this.(BoundMethodObjectInternal).getSelf() }
|
||||
Value getSelf() { result = super.getSelf() }
|
||||
|
||||
/** Gets the parameter node that will be used for `self`. */
|
||||
NameNode getSelfParameter() { result = this.(BoundMethodObjectInternal).getSelfParameter() }
|
||||
NameNode getSelfParameter() { result = super.getSelfParameter() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -831,12 +827,10 @@ class BuiltinMethodValue extends FunctionValue {
|
||||
/**
|
||||
* A class representing sequence objects with a length and tracked items.
|
||||
*/
|
||||
class SequenceValue extends Value {
|
||||
SequenceValue() { this instanceof SequenceObjectInternal }
|
||||
class SequenceValue extends Value instanceof SequenceObjectInternal {
|
||||
Value getItem(int n) { result = super.getItem(n) }
|
||||
|
||||
Value getItem(int n) { result = this.(SequenceObjectInternal).getItem(n) }
|
||||
|
||||
int length() { result = this.(SequenceObjectInternal).length() }
|
||||
int length() { result = super.length() }
|
||||
}
|
||||
|
||||
/** A class representing tuple objects */
|
||||
@@ -887,14 +881,12 @@ class NumericValue extends Value {
|
||||
* https://docs.python.org/3/howto/descriptor.html#properties
|
||||
* https://docs.python.org/3/library/functions.html#property
|
||||
*/
|
||||
class PropertyValue extends Value {
|
||||
PropertyValue() { this instanceof PropertyInternal }
|
||||
class PropertyValue extends Value instanceof PropertyInternal {
|
||||
CallableValue getGetter() { result = super.getGetter() }
|
||||
|
||||
CallableValue getGetter() { result = this.(PropertyInternal).getGetter() }
|
||||
CallableValue getSetter() { result = super.getSetter() }
|
||||
|
||||
CallableValue getSetter() { result = this.(PropertyInternal).getSetter() }
|
||||
|
||||
CallableValue getDeleter() { result = this.(PropertyInternal).getDeleter() }
|
||||
CallableValue getDeleter() { result = super.getDeleter() }
|
||||
}
|
||||
|
||||
/** A method-resolution-order sequence of classes */
|
||||
|
||||
@@ -1195,16 +1195,22 @@ module InterProceduralPointsTo {
|
||||
ControlFlowNode argument, PointsToContext caller, ParameterDefinition param,
|
||||
PointsToContext callee
|
||||
) {
|
||||
PointsToInternal::pointsTo(argument, caller, _, _) and
|
||||
exists(CallNode call, Function func, int offset |
|
||||
callsite_calls_function(call, caller, func, callee, offset)
|
||||
|
|
||||
exists(string name |
|
||||
argument = call.getArgByName(name) and
|
||||
param.getParameter() = func.getArgByName(name)
|
||||
function_parameter_name(func, param, name)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate function_parameter_name(Function func, ParameterDefinition param, string name) {
|
||||
param.getParameter() = func.getArgByName(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the `call` with context `caller` calls the function `scope` in context `callee`
|
||||
* and the offset from argument to parameter is `parameter_offset`
|
||||
|
||||
@@ -26,7 +26,11 @@ class PathNotNormalizedConfiguration extends TaintTracking::Configuration {
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
|
||||
|
||||
override predicate isSanitizer(DataFlow::Node node) { node instanceof Path::PathNormalization }
|
||||
override predicate isSanitizer(DataFlow::Node node) {
|
||||
node instanceof Sanitizer
|
||||
or
|
||||
node instanceof Path::PathNormalization
|
||||
}
|
||||
|
||||
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
|
||||
guard instanceof SanitizerGuard
|
||||
@@ -52,6 +56,8 @@ class FirstNormalizationConfiguration extends TaintTracking::Configuration {
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof Path::PathNormalization }
|
||||
|
||||
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
|
||||
|
||||
override predicate isSanitizerOut(DataFlow::Node node) { node instanceof Path::PathNormalization }
|
||||
|
||||
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
|
||||
@@ -67,6 +73,8 @@ class NormalizedPathNotCheckedConfiguration extends TaintTracking2::Configuratio
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
|
||||
|
||||
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
|
||||
|
||||
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
|
||||
guard instanceof Path::SafeAccessCheck
|
||||
or
|
||||
|
||||
@@ -32,6 +32,16 @@ module PathInjection {
|
||||
*/
|
||||
abstract class Sink extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A sanitizer for "path injection" vulnerabilities.
|
||||
*
|
||||
* This should only be used for things like calls to library functions that perform their own
|
||||
* (correct) normalization/escaping of untrusted paths.
|
||||
*
|
||||
* Please also see `Path::SafeAccessCheck` and `Path::PathNormalization` Concepts.
|
||||
*/
|
||||
abstract class Sanitizer extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A sanitizer guard for "path injection" vulnerabilities.
|
||||
*/
|
||||
|
||||
@@ -42,6 +42,13 @@ module SqlInjection {
|
||||
*/
|
||||
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
|
||||
|
||||
/**
|
||||
* A SQL statement of a SQL construction, considered as a flow sink.
|
||||
*/
|
||||
class SqlConstructionAsSink extends Sink {
|
||||
SqlConstructionAsSink() { this = any(SqlConstruction c).getSql() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A SQL statement of a SQL execution, considered as a flow sink.
|
||||
*/
|
||||
@@ -49,13 +56,6 @@ module SqlInjection {
|
||||
SqlExecutionAsSink() { this = any(SqlExecution e).getSql() }
|
||||
}
|
||||
|
||||
/**
|
||||
* The text argument of a SQLAlchemy TextClause construction, considered as a flow sink.
|
||||
*/
|
||||
class TextArgAsSink extends Sink {
|
||||
TextArgAsSink() { this = any(SqlAlchemy::TextClause::TextClauseConstruction tcc).getTextArg() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
*/
|
||||
|
||||
@@ -58,7 +58,7 @@ module HeuristicNames {
|
||||
*/
|
||||
string maybeAccountInfo() {
|
||||
result = "(?is).*acc(ou)?nt.*" or
|
||||
result = "(?is).*(puid|username|userid).*" or
|
||||
result = "(?is).*(puid|username|userid|session(id|key)).*" or
|
||||
result = "(?s).*([uU]|^|_|[a-z](?=U))([uU][iI][dD]).*"
|
||||
}
|
||||
|
||||
|
||||
@@ -144,12 +144,10 @@ class ReModulePointToExtension extends PointsToExtension {
|
||||
private predicate pointsTo_helper(Context context) { context.appliesTo(this) }
|
||||
}
|
||||
|
||||
deprecated private class BackwardCompatiblePointToExtension extends PointsToExtension {
|
||||
BackwardCompatiblePointToExtension() { this instanceof CustomPointsToFact }
|
||||
|
||||
deprecated private class BackwardCompatiblePointToExtension extends PointsToExtension instanceof CustomPointsToFact {
|
||||
override predicate pointsTo(Context context, ObjectInternal value, ControlFlowNode origin) {
|
||||
exists(Object obj, ClassObject cls |
|
||||
this.(CustomPointsToFact).pointsTo(context, obj, cls, origin)
|
||||
CustomPointsToFact.super.pointsTo(context, obj, cls, origin)
|
||||
|
|
||||
value.getBuiltin() = obj
|
||||
or
|
||||
|
||||
@@ -84,7 +84,7 @@ private predicate property_getter(CallNode decorated, FunctionObject getter) {
|
||||
private predicate property_setter(CallNode decorated, FunctionObject setter) {
|
||||
property_getter(decorated, _) and
|
||||
exists(CallNode setter_call, AttrNode prop_setter |
|
||||
prop_setter.getObject("setter").refersTo(decorated.(Object))
|
||||
prop_setter.getObject("setter").refersTo(decorated)
|
||||
|
|
||||
setter_call.getArg(0).refersTo(setter) and
|
||||
setter_call.getFunction() = prop_setter
|
||||
@@ -97,7 +97,7 @@ private predicate property_setter(CallNode decorated, FunctionObject setter) {
|
||||
private predicate property_deleter(CallNode decorated, FunctionObject deleter) {
|
||||
property_getter(decorated, _) and
|
||||
exists(CallNode deleter_call, AttrNode prop_deleter |
|
||||
prop_deleter.getObject("deleter").refersTo(decorated.(Object))
|
||||
prop_deleter.getObject("deleter").refersTo(decorated)
|
||||
|
|
||||
deleter_call.getArg(0).refersTo(deleter) and
|
||||
deleter_call.getFunction() = prop_deleter
|
||||
|
||||
@@ -78,5 +78,5 @@ private predicate tracking_step(ControlFlowNode src, ControlFlowNode dest) {
|
||||
or
|
||||
tracked_call_step(src, dest)
|
||||
or
|
||||
dest.refersTo(src.(Object))
|
||||
dest.refersTo(src)
|
||||
}
|
||||
|
||||
@@ -99,14 +99,14 @@ private ControlFlowNode get_a_call(Value callable) {
|
||||
|
||||
/** Gets the function object corresponding to the given class or function. */
|
||||
FunctionObject get_function_or_initializer_objectapi(Object func_or_cls) {
|
||||
result = func_or_cls.(FunctionObject)
|
||||
result = func_or_cls
|
||||
or
|
||||
result = func_or_cls.(ClassObject).declaredAttribute("__init__")
|
||||
}
|
||||
|
||||
/** Gets the function object corresponding to the given class or function. */
|
||||
FunctionValue get_function_or_initializer(Value func_or_cls) {
|
||||
result = func_or_cls.(FunctionValue)
|
||||
result = func_or_cls
|
||||
or
|
||||
result = func_or_cls.(ClassValue).declaredAttribute("__init__")
|
||||
}
|
||||
|
||||
@@ -210,9 +210,9 @@ class CommentedOutCodeBlock extends @py_comment {
|
||||
|
||||
/** Whether this commented-out code block is likely to be example code embedded in a larger comment. */
|
||||
predicate maybeExampleCode() {
|
||||
exists(CommentBlock block | block.contains(this.(Comment)) |
|
||||
exists(CommentBlock block | block.contains(this) |
|
||||
exists(int all_code |
|
||||
all_code = sum(CommentedOutCodeBlock code | block.contains(code.(Comment)) | code.length()) and
|
||||
all_code = sum(CommentedOutCodeBlock code | block.contains(code) | code.length()) and
|
||||
/* This ratio may need fine tuning */
|
||||
block.length() > all_code * 2
|
||||
)
|
||||
|
||||
@@ -11,17 +11,46 @@
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.web.Http
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
FunctionValue requestFunction() { result = Module::named("requests").attr(httpVerbLower()) }
|
||||
/**
|
||||
* Gets a call to a method that makes an outgoing request using the `requests` module,
|
||||
* such as `requests.get` or `requests.put`, with the specified HTTP verb `verb`
|
||||
*/
|
||||
DataFlow::CallCfgNode outgoingRequestCall(string verb) {
|
||||
verb = HTTP::httpVerbLower() and
|
||||
result = API::moduleImport("requests").getMember(verb).getACall()
|
||||
}
|
||||
|
||||
/** requests treats None as the default and all other "falsey" values as False */
|
||||
predicate falseNotNone(Value v) { v.getDefiniteBooleanValue() = false and not v = Value::none_() }
|
||||
/** Gets the "verfiy" argument to a outgoingRequestCall. */
|
||||
DataFlow::Node verifyArg(DataFlow::CallCfgNode call) {
|
||||
call = outgoingRequestCall(_) and
|
||||
result = call.getArgByName("verify")
|
||||
}
|
||||
|
||||
from CallNode call, FunctionValue func, Value falsey, ControlFlowNode origin
|
||||
/** Gets a back-reference to the verify argument `arg`. */
|
||||
private DataFlow::TypeTrackingNode verifyArgBacktracker(
|
||||
DataFlow::TypeBackTracker t, DataFlow::Node arg
|
||||
) {
|
||||
t.start() and
|
||||
arg = verifyArg(_) and
|
||||
result = arg.getALocalSource()
|
||||
or
|
||||
exists(DataFlow::TypeBackTracker t2 | result = verifyArgBacktracker(t2, arg).backtrack(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a back-reference to the verify argument `arg`. */
|
||||
DataFlow::LocalSourceNode verifyArgBacktracker(DataFlow::Node arg) {
|
||||
result = verifyArgBacktracker(DataFlow::TypeBackTracker::end(), arg)
|
||||
}
|
||||
|
||||
from DataFlow::CallCfgNode call, DataFlow::Node falseyOrigin, string verb
|
||||
where
|
||||
func = requestFunction() and
|
||||
func.getACall() = call and
|
||||
falseNotNone(falsey) and
|
||||
call.getArgByName("verify").pointsTo(falsey, origin)
|
||||
select call, "Call to $@ with verify=$@", func, "requests." + func.getName(), origin, "False"
|
||||
call = outgoingRequestCall(verb) and
|
||||
falseyOrigin = verifyArgBacktracker(verifyArg(call)) and
|
||||
// requests treats `None` as the default and all other "falsey" values as `False`.
|
||||
falseyOrigin.asExpr().(ImmutableLiteral).booleanValue() = false and
|
||||
not falseyOrigin.asExpr() instanceof None
|
||||
select call, "Call to requests." + verb + " with verify=$@", falseyOrigin, "False"
|
||||
|
||||
@@ -70,10 +70,11 @@ predicate same_attribute(Attribute a1, Attribute a2) {
|
||||
not is_property_access(a1)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
Comment pyflakes_comment() { result.getText().toLowerCase().matches("%pyflakes%") }
|
||||
|
||||
int pyflakes_commented_line(File file) {
|
||||
exists(Comment c | c.getText().toLowerCase().matches("%pyflakes%") |
|
||||
c.getLocation().hasLocationInfo(file.getAbsolutePath(), result, _, _, _)
|
||||
)
|
||||
pyflakes_comment().getLocation().hasLocationInfo(file.getAbsolutePath(), result, _, _, _)
|
||||
}
|
||||
|
||||
predicate pyflakes_commented(AssignStmt assignment) {
|
||||
|
||||
@@ -10,7 +10,7 @@ private import experimental.semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
|
||||
private module PrivateDjango {
|
||||
private module ExperimentalPrivateDjango {
|
||||
private module django {
|
||||
API::Node http() { result = API::moduleImport("django").getMember("http") }
|
||||
|
||||
|
||||
23
python/ql/src/meta/alerts/RequestHandlers.ql
Normal file
23
python/ql/src/meta/alerts/RequestHandlers.ql
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @name Request Handlers
|
||||
* @description HTTP Server Request Handlers
|
||||
* @kind problem
|
||||
* @problem.severity recommendation
|
||||
* @id py/meta/alerts/request-handlers
|
||||
* @tags meta
|
||||
* @precision very-low
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import meta.MetaMetrics
|
||||
|
||||
from HTTP::Server::RequestHandler requestHandler, string title
|
||||
where
|
||||
not requestHandler.getLocation().getFile() instanceof IgnoredFile and
|
||||
if requestHandler.isMethod()
|
||||
then
|
||||
title = "Method " + requestHandler.getScope().(Class).getName() + "." + requestHandler.getName()
|
||||
else title = requestHandler.toString()
|
||||
select requestHandler, "RequestHandler: " + title
|
||||
@@ -3,13 +3,25 @@ import pkg # $ use=moduleImport("pkg")
|
||||
async def foo():
|
||||
coro = pkg.async_func() # $ use=moduleImport("pkg").getMember("async_func").getReturn()
|
||||
coro # $ use=moduleImport("pkg").getMember("async_func").getReturn()
|
||||
result = await coro # $ use=moduleImport("pkg").getMember("async_func").getReturn().getAwaited()
|
||||
result # $ use=moduleImport("pkg").getMember("async_func").getReturn().getAwaited()
|
||||
return result # $ use=moduleImport("pkg").getMember("async_func").getReturn().getAwaited()
|
||||
result = await coro # $ use=moduleImport("pkg").getMember("async_func").getReturn().getAwaited() awaited=moduleImport("pkg").getMember("async_func").getReturn()
|
||||
result # $ use=moduleImport("pkg").getMember("async_func").getReturn().getAwaited() awaited=moduleImport("pkg").getMember("async_func").getReturn()
|
||||
return result # $ use=moduleImport("pkg").getMember("async_func").getReturn().getAwaited() awaited=moduleImport("pkg").getMember("async_func").getReturn()
|
||||
|
||||
async def bar():
|
||||
result = await pkg.async_func() # $ use=moduleImport("pkg").getMember("async_func").getReturn().getAwaited()
|
||||
return result # $ use=moduleImport("pkg").getMember("async_func").getReturn().getAwaited()
|
||||
result = await pkg.async_func() # $ use=moduleImport("pkg").getMember("async_func").getReturn().getAwaited() awaited=moduleImport("pkg").getMember("async_func").getReturn()
|
||||
return result # $ use=moduleImport("pkg").getMember("async_func").getReturn().getAwaited() awaited=moduleImport("pkg").getMember("async_func").getReturn()
|
||||
|
||||
async def test_async_with():
|
||||
async with pkg.async_func() as result: # $ use=moduleImport("pkg").getMember("async_func").getReturn().getAwaited() awaited=moduleImport("pkg").getMember("async_func").getReturn()
|
||||
return result # $ use=moduleImport("pkg").getMember("async_func").getReturn().getAwaited() awaited=moduleImport("pkg").getMember("async_func").getReturn()
|
||||
|
||||
async def test_async_for():
|
||||
async for _ in pkg.async_func(): # $ use=moduleImport("pkg").getMember("async_func").getReturn() awaited=moduleImport("pkg").getMember("async_func").getReturn()
|
||||
pass
|
||||
|
||||
coro = pkg.async_func() # $ use=moduleImport("pkg").getMember("async_func").getReturn()
|
||||
async for _ in coro: # $ use=moduleImport("pkg").getMember("async_func").getReturn() MISSING: awaited=moduleImport("pkg").getMember("async_func").getReturn()
|
||||
pass
|
||||
|
||||
def check_annotations():
|
||||
# Just to make sure how annotations should look like :)
|
||||
|
||||
26
python/ql/test/experimental/dataflow/ApiGraphs/awaited.ql
Normal file
26
python/ql/test/experimental/dataflow/ApiGraphs/awaited.ql
Normal file
@@ -0,0 +1,26 @@
|
||||
import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import TestUtilities.InlineExpectationsTest
|
||||
import semmle.python.ApiGraphs
|
||||
|
||||
class AwaitedTest extends InlineExpectationsTest {
|
||||
AwaitedTest() { this = "AwaitedTest" }
|
||||
|
||||
override string getARelevantTag() { result = "awaited" }
|
||||
|
||||
override predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(API::Node awaited, DataFlow::Node use, API::Node pred |
|
||||
awaited = pred.getAwaited() and
|
||||
use = awaited.getAUse() and
|
||||
location = use.getLocation() and
|
||||
// Module variable nodes have no suitable location, so it's best to simply exclude them entirely
|
||||
// from the inline tests.
|
||||
not use instanceof DataFlow::ModuleVariableNode and
|
||||
exists(location.getFile().getRelativePath())
|
||||
|
|
||||
tag = "awaited" and
|
||||
value = pred.getPath() and
|
||||
element = use.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,16 @@ class TestTaintTrackingConfiguration extends TaintTracking::Configuration {
|
||||
TestTaintTrackingConfiguration() { this = "TestTaintTrackingConfiguration" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) {
|
||||
// Standard sources
|
||||
source.(DataFlow::CfgNode).getNode().(NameNode).getId() in [
|
||||
"TAINTED_STRING", "TAINTED_BYTES", "TAINTED_LIST", "TAINTED_DICT"
|
||||
]
|
||||
or
|
||||
// User defined sources
|
||||
exists(CallNode call |
|
||||
call.getFunction().(NameNode).getId() = "taint" and
|
||||
source.(DataFlow::CfgNode).getNode() = call.getAnArg()
|
||||
)
|
||||
}
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Add taintlib to PATH so it can be imported during runtime without any hassle
|
||||
import sys; import os; sys.path.append(os.path.dirname(os.path.dirname((__file__))))
|
||||
from taintlib import *
|
||||
|
||||
# This has no runtime impact, but allows autocomplete to work
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from ..taintlib import *
|
||||
|
||||
|
||||
# Actual tests
|
||||
|
||||
async def tainted_coro():
|
||||
return TAINTED_STRING
|
||||
|
||||
async def test_await():
|
||||
coro = tainted_coro()
|
||||
taint(coro)
|
||||
s = await coro
|
||||
ensure_tainted(coro, s) # $ tainted
|
||||
|
||||
|
||||
class AsyncContext:
|
||||
async def __aenter__(self):
|
||||
return TAINTED_STRING
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
async def test_async_with():
|
||||
ctx = AsyncContext()
|
||||
taint(ctx)
|
||||
async with ctx as tainted:
|
||||
ensure_tainted(tainted) # $ tainted
|
||||
|
||||
|
||||
class AsyncIter:
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
raise StopAsyncIteration
|
||||
|
||||
async def test_async_for():
|
||||
iter = AsyncIter()
|
||||
taint(iter)
|
||||
async for tainted in iter:
|
||||
ensure_tainted(tainted) # $ tainted
|
||||
|
||||
|
||||
|
||||
# Make tests runable
|
||||
import asyncio
|
||||
|
||||
asyncio.run(test_await())
|
||||
asyncio.run(test_async_with())
|
||||
asyncio.run(test_async_for())
|
||||
@@ -0,0 +1,30 @@
|
||||
# Add taintlib to PATH so it can be imported during runtime without any hassle
|
||||
import sys; import os; sys.path.append(os.path.dirname(os.path.dirname((__file__))))
|
||||
from taintlib import *
|
||||
|
||||
# This has no runtime impact, but allows autocomplete to work
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from ..taintlib import *
|
||||
|
||||
|
||||
# Actual tests
|
||||
|
||||
class Iter:
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
raise StopIteration
|
||||
|
||||
def test_for():
|
||||
iter = Iter()
|
||||
taint(iter)
|
||||
for tainted in iter:
|
||||
ensure_tainted(tainted) # $ tainted
|
||||
|
||||
|
||||
|
||||
# Make tests runable
|
||||
|
||||
test_for()
|
||||
@@ -0,0 +1,60 @@
|
||||
# Add taintlib to PATH so it can be imported during runtime without any hassle
|
||||
import sys; import os; sys.path.append(os.path.dirname(os.path.dirname((__file__))))
|
||||
from taintlib import *
|
||||
|
||||
# This has no runtime impact, but allows autocomplete to work
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from ..taintlib import *
|
||||
|
||||
|
||||
# Actual tests
|
||||
|
||||
class Context:
|
||||
def __enter__(self):
|
||||
return ""
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
def test_with():
|
||||
ctx = Context()
|
||||
taint(ctx)
|
||||
with ctx as tainted:
|
||||
ensure_tainted(tainted) # $ tainted
|
||||
|
||||
class Context_taint:
|
||||
def __enter__(self):
|
||||
return TAINTED_STRING
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
def test_with_taint():
|
||||
ctx = Context_taint()
|
||||
with ctx as tainted:
|
||||
ensure_tainted(tainted) # $ MISSING: tainted
|
||||
|
||||
|
||||
class Context_arg:
|
||||
def __init__(self, arg):
|
||||
self.arg = arg
|
||||
|
||||
def __enter__(self):
|
||||
return self.arg
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
def test_with_arg():
|
||||
ctx = Context_arg(TAINTED_STRING)
|
||||
with ctx as tainted:
|
||||
ensure_tainted(tainted) # $ tainted
|
||||
|
||||
|
||||
|
||||
# Make tests runable
|
||||
|
||||
test_with()
|
||||
test_with_taint()
|
||||
test_with_arg()
|
||||
@@ -5,6 +5,11 @@ TAINTED_DICT = {"name": TAINTED_STRING, "some key": "foo"}
|
||||
|
||||
NOT_TAINTED = "NOT_TAINTED"
|
||||
|
||||
# Use this to force expressions to be tainted
|
||||
def taint(*args):
|
||||
pass
|
||||
|
||||
|
||||
def ensure_tainted(*args):
|
||||
print("- ensure_tainted")
|
||||
for i, arg in enumerate(args):
|
||||
|
||||
@@ -128,6 +128,24 @@ class CodeExecutionTest extends InlineExpectationsTest {
|
||||
}
|
||||
}
|
||||
|
||||
class SqlConstructionTest extends InlineExpectationsTest {
|
||||
SqlConstructionTest() { this = "SqlConstructionTest" }
|
||||
|
||||
override string getARelevantTag() { result = "constructedSql" }
|
||||
|
||||
override predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(location.getFile().getRelativePath()) and
|
||||
exists(SqlConstruction e, DataFlow::Node sql |
|
||||
exists(location.getFile().getRelativePath()) and
|
||||
sql = e.getSql() and
|
||||
location = e.getLocation() and
|
||||
element = sql.toString() and
|
||||
value = prettyNodeForInlineTest(sql) and
|
||||
tag = "constructedSql"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SqlExecutionTest extends InlineExpectationsTest {
|
||||
SqlExecutionTest() { this = "SqlExecutionTest" }
|
||||
|
||||
|
||||
@@ -30,24 +30,36 @@ DataFlow::Node shouldNotBeTainted() {
|
||||
)
|
||||
}
|
||||
|
||||
class TestTaintTrackingConfiguration extends TaintTracking::Configuration {
|
||||
TestTaintTrackingConfiguration() { this = "TestTaintTrackingConfiguration" }
|
||||
// this module allows the configuration to be imported in other `.ql` files without the
|
||||
// top level query predicates of this file coming into scope.
|
||||
module Conf {
|
||||
class TestTaintTrackingConfiguration extends TaintTracking::Configuration {
|
||||
TestTaintTrackingConfiguration() { this = "TestTaintTrackingConfiguration" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) {
|
||||
source.asCfgNode().(NameNode).getId() in [
|
||||
"TAINTED_STRING", "TAINTED_BYTES", "TAINTED_LIST", "TAINTED_DICT"
|
||||
]
|
||||
or
|
||||
source instanceof RemoteFlowSource
|
||||
}
|
||||
override predicate isSource(DataFlow::Node source) {
|
||||
source.asCfgNode().(NameNode).getId() in [
|
||||
"TAINTED_STRING", "TAINTED_BYTES", "TAINTED_LIST", "TAINTED_DICT"
|
||||
]
|
||||
or
|
||||
// User defined sources
|
||||
exists(CallNode call |
|
||||
call.getFunction().(NameNode).getId() = "taint" and
|
||||
source.(DataFlow::CfgNode).getNode() = call.getAnArg()
|
||||
)
|
||||
or
|
||||
source instanceof RemoteFlowSource
|
||||
}
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) {
|
||||
sink = shouldBeTainted()
|
||||
or
|
||||
sink = shouldNotBeTainted()
|
||||
override predicate isSink(DataFlow::Node sink) {
|
||||
sink = shouldBeTainted()
|
||||
or
|
||||
sink = shouldNotBeTainted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import Conf
|
||||
|
||||
class InlineTaintTest extends InlineExpectationsTest {
|
||||
InlineTaintTest() { this = "InlineTaintTest" }
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
edges
|
||||
nodes
|
||||
subpaths
|
||||
#select
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @kind path-problem
|
||||
*/
|
||||
|
||||
// This query is for debugging InlineTaintTestFailures.
|
||||
// The intended usage is
|
||||
// 1. load the database of the failing test
|
||||
// 2. run this query to see actual paths
|
||||
// 3. if necessary, look at partial paths by (un)commenting appropriate lines
|
||||
import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import experimental.meta.InlineTaintTest::Conf
|
||||
// import DataFlow::PartialPathGraph
|
||||
import DataFlow::PathGraph
|
||||
|
||||
class Conf extends TestTaintTrackingConfiguration {
|
||||
override int explorationLimit() { result = 5 }
|
||||
}
|
||||
|
||||
// from Conf config, DataFlow::PartialPathNode source, DataFlow::PartialPathNode sink
|
||||
// where config.hasPartialFlow(source, sink, _)
|
||||
from Conf config, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where config.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink, "This node receives taint from $@.", source.getNode(),
|
||||
"this source"
|
||||
@@ -0,0 +1,2 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
33
python/ql/test/library-tests/frameworks/aiomysql/test.py
Normal file
33
python/ql/test/library-tests/frameworks/aiomysql/test.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import aiomysql
|
||||
|
||||
# Only a cursor can execute sql.
|
||||
async def test_cursor():
|
||||
# Create connection directly
|
||||
conn = await aiomysql.connect()
|
||||
cur = await conn.cursor()
|
||||
await cur.execute("sql") # $ getSql="sql" constructedSql="sql"
|
||||
|
||||
# Create connection via pool
|
||||
async with aiomysql.create_pool() as pool:
|
||||
# Create Cursor via Connection
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute("sql") # $ getSql="sql" constructedSql="sql"
|
||||
|
||||
# Create Cursor directly
|
||||
async with pool.cursor() as cur:
|
||||
await cur.execute("sql") # $ getSql="sql" constructedSql="sql"
|
||||
|
||||
# variants using as few `async with` as possible
|
||||
pool = await aiomysql.create_pool()
|
||||
conn = await pool.acquire()
|
||||
cur = await conn.cursor()
|
||||
await cur.execute("sql") # $ getSql="sql" constructedSql="sql"
|
||||
|
||||
# Test SQLAlchemy integration
|
||||
from aiomysql.sa import create_engine
|
||||
|
||||
async def test_engine():
|
||||
engine = await create_engine()
|
||||
conn = await engine.acquire()
|
||||
await conn.execute("sql") # $ getSql="sql" constructedSql="sql"
|
||||
@@ -0,0 +1,2 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
33
python/ql/test/library-tests/frameworks/aiopg/test.py
Normal file
33
python/ql/test/library-tests/frameworks/aiopg/test.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import aiopg
|
||||
|
||||
# Only a cursor can execute sql.
|
||||
async def test_cursor():
|
||||
# Create connection directly
|
||||
conn = await aiopg.connect()
|
||||
cur = await conn.cursor()
|
||||
await cur.execute("sql") # $ getSql="sql" constructedSql="sql"
|
||||
|
||||
# Create connection via pool
|
||||
async with aiopg.create_pool() as pool:
|
||||
# Create Cursor via Connection
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute("sql") # $ getSql="sql" constructedSql="sql"
|
||||
|
||||
# Create Cursor directly
|
||||
async with pool.cursor() as cur:
|
||||
await cur.execute("sql") # $ getSql="sql" constructedSql="sql"
|
||||
|
||||
# variants using as few `async with` as possible
|
||||
pool = await aiopg.create_pool()
|
||||
conn = await pool.acquire()
|
||||
cur = await conn.cursor()
|
||||
await cur.execute("sql") # $ getSql="sql" constructedSql="sql"
|
||||
|
||||
# Test SQLAlchemy integration
|
||||
from aiopg.sa import create_engine
|
||||
|
||||
async def test_engine():
|
||||
engine = await create_engine()
|
||||
conn = await engine.acquire()
|
||||
await conn.execute("sql") # $ getSql="sql" constructedSql="sql"
|
||||
@@ -0,0 +1,2 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
105
python/ql/test/library-tests/frameworks/asyncpg/test.py
Normal file
105
python/ql/test/library-tests/frameworks/asyncpg/test.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import asyncio
|
||||
import asyncpg
|
||||
|
||||
async def test_connection():
|
||||
conn = await asyncpg.connect()
|
||||
|
||||
try:
|
||||
# The file-like object is passed in as a keyword-only argument.
|
||||
# See https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.connection.Connection.copy_from_query
|
||||
await conn.copy_from_query("sql", output="filepath") # $ getSql="sql" getAPathArgument="filepath"
|
||||
await conn.copy_from_query("sql", "arg1", "arg2", output="filepath") # $ getSql="sql" getAPathArgument="filepath"
|
||||
|
||||
await conn.copy_from_table("table", output="filepath") # $ getAPathArgument="filepath"
|
||||
await conn.copy_to_table("table", source="filepath") # $ getAPathArgument="filepath"
|
||||
|
||||
await conn.execute("sql") # $ getSql="sql"
|
||||
await conn.executemany("sql") # $ getSql="sql"
|
||||
await conn.fetch("sql") # $ getSql="sql"
|
||||
await conn.fetchrow("sql") # $ getSql="sql"
|
||||
await conn.fetchval("sql") # $ getSql="sql"
|
||||
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
async def test_prepared_statement():
|
||||
conn = await asyncpg.connect()
|
||||
|
||||
try:
|
||||
pstmt = await conn.prepare("psql") # $ constructedSql="psql"
|
||||
pstmt.executemany() # $ getSql="psql"
|
||||
pstmt.fetch() # $ getSql="psql"
|
||||
pstmt.fetchrow() # $ getSql="psql"
|
||||
pstmt.fetchval() # $ getSql="psql"
|
||||
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
# The sql statement is executed when the `CursorFactory` (obtained by e.g. `conn.cursor()`) is awaited.
|
||||
# See https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.cursor.CursorFactory
|
||||
async def test_cursor():
|
||||
conn = await asyncpg.connect()
|
||||
|
||||
try:
|
||||
async with conn.transaction():
|
||||
cursor = await conn.cursor("sql") # $ getSql="sql" constructedSql="sql"
|
||||
await cursor.fetch()
|
||||
|
||||
pstmt = await conn.prepare("psql") # $ constructedSql="psql"
|
||||
pcursor = await pstmt.cursor() # $ getSql="psql"
|
||||
await pcursor.fetch()
|
||||
|
||||
async for record in conn.cursor("sql"): # $ getSql="sql" constructedSql="sql"
|
||||
pass
|
||||
|
||||
async for record in pstmt.cursor(): # $ getSql="psql"
|
||||
pass
|
||||
|
||||
cursor_factory = conn.cursor("sql") # $ constructedSql="sql"
|
||||
cursor = await cursor_factory # $ getSql="sql"
|
||||
|
||||
pcursor_factory = pstmt.cursor()
|
||||
pcursor = await pcursor_factory # $ getSql="psql"
|
||||
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
async def test_connection_pool():
|
||||
pool = await asyncpg.create_pool()
|
||||
|
||||
try:
|
||||
await pool.copy_from_query("sql", output="filepath") # $ getSql="sql" getAPathArgument="filepath"
|
||||
await pool.copy_from_query("sql", "arg1", "arg2", output="filepath") # $ getSql="sql" getAPathArgument="filepath"
|
||||
await pool.copy_from_table("table", output="filepath") # $ getAPathArgument="filepath"
|
||||
await pool.copy_to_table("table", source="filepath") # $ getAPathArgument="filepath"
|
||||
|
||||
await pool.execute("sql") # $ getSql="sql"
|
||||
await pool.executemany("sql") # $ getSql="sql"
|
||||
await pool.fetch("sql") # $ getSql="sql"
|
||||
await pool.fetchrow("sql") # $ getSql="sql"
|
||||
await pool.fetchval("sql") # $ getSql="sql"
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute("sql") # $ getSql="sql"
|
||||
|
||||
conn = await pool.acquire()
|
||||
try:
|
||||
await conn.fetch("sql") # $ getSql="sql"
|
||||
finally:
|
||||
await pool.release(conn)
|
||||
|
||||
finally:
|
||||
await pool.close()
|
||||
|
||||
async with asyncpg.create_pool() as pool:
|
||||
await pool.execute("sql") # $ getSql="sql"
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute("sql") # $ getSql="sql"
|
||||
|
||||
conn = await pool.acquire()
|
||||
try:
|
||||
await conn.fetch("sql") # $ getSql="sql"
|
||||
finally:
|
||||
await pool.release(conn)
|
||||
@@ -0,0 +1,12 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
|
||||
class DedicatedResponseTest extends HttpServerHttpResponseTest {
|
||||
DedicatedResponseTest() { file.getShortName() = "response_test.py" }
|
||||
}
|
||||
|
||||
class OtherResponseTest extends HttpServerHttpResponseTest {
|
||||
OtherResponseTest() { not this instanceof DedicatedResponseTest }
|
||||
|
||||
override string getARelevantTag() { result = "HttpResponse" }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
argumentToEnsureNotTaintedNotMarkedAsSpurious
|
||||
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
|
||||
failures
|
||||
@@ -0,0 +1 @@
|
||||
import experimental.meta.InlineTaintTest
|
||||
66
python/ql/test/library-tests/frameworks/fastapi/basic.py
Normal file
66
python/ql/test/library-tests/frameworks/fastapi/basic.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# Taking inspiration from https://realpython.com/fastapi-python-web-apis/
|
||||
|
||||
# run with
|
||||
# uvicorn basic:app --reload
|
||||
# Then visit http://127.0.0.1:8000/docs and http://127.0.0.1:8000/redoc
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/") # $ routeSetup="/"
|
||||
async def root(): # $ requestHandler
|
||||
return {"message": "Hello World"} # $ HttpResponse
|
||||
|
||||
@app.get("/non-async") # $ routeSetup="/non-async"
|
||||
def non_async(): # $ requestHandler
|
||||
return {"message": "non-async"} # $ HttpResponse
|
||||
|
||||
@app.get(path="/kw-arg") # $ routeSetup="/kw-arg"
|
||||
def kw_arg(): # $ requestHandler
|
||||
return {"message": "kw arg"} # $ HttpResponse
|
||||
|
||||
@app.get("/foo/{foo_id}") # $ routeSetup="/foo/{foo_id}"
|
||||
async def get_foo(foo_id: int): # $ requestHandler routedParameter=foo_id
|
||||
# FastAPI does data validation (with `pydantic` PyPI package) under the hood based
|
||||
# on the type annotation we did for `foo_id`, so it will auto-reject anything that's
|
||||
# not an int.
|
||||
return {"foo_id": foo_id} # $ HttpResponse
|
||||
|
||||
# this will work as query param, so `/bar?bar_id=123`
|
||||
@app.get("/bar") # $ routeSetup="/bar"
|
||||
async def get_bar(bar_id: int = 42): # $ requestHandler routedParameter=bar_id
|
||||
return {"bar_id": bar_id} # $ HttpResponse
|
||||
|
||||
# The big deal is that FastAPI works so well together with pydantic, so you can do stuff like this
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
is_offer: Optional[bool] = None
|
||||
|
||||
@app.post("/items/") # $ routeSetup="/items/"
|
||||
async def create_item(item: Item): # $ requestHandler routedParameter=item
|
||||
# Note: calling `item` a routed parameter is slightly untrue, since it doesn't come
|
||||
# from the URL itself, but from the body of the POST request
|
||||
return item # $ HttpResponse
|
||||
|
||||
# this also works fine
|
||||
@app.post("/2items") # $ routeSetup="/2items"
|
||||
async def create_item2(item1: Item, item2: Item): # $ requestHandler routedParameter=item1 routedParameter=item2
|
||||
return (item1, item2) # $ HttpResponse
|
||||
|
||||
@app.api_route("/baz/{baz_id}", methods=["GET"]) # $ routeSetup="/baz/{baz_id}"
|
||||
async def get_baz(baz_id: int): # $ requestHandler routedParameter=baz_id
|
||||
return {"baz_id2": baz_id} # $ HttpResponse
|
||||
|
||||
# Docs:
|
||||
# see https://fastapi.tiangolo.com/tutorial/path-params/
|
||||
|
||||
# Things we should look at supporting:
|
||||
# - https://fastapi.tiangolo.com/tutorial/dependencies/
|
||||
# - https://fastapi.tiangolo.com/tutorial/background-tasks/
|
||||
# - https://fastapi.tiangolo.com/tutorial/middleware/
|
||||
# - https://fastapi.tiangolo.com/tutorial/encoder/
|
||||
145
python/ql/test/library-tests/frameworks/fastapi/response_test.py
Normal file
145
python/ql/test/library-tests/frameworks/fastapi/response_test.py
Normal file
@@ -0,0 +1,145 @@
|
||||
# see https://fastapi.tiangolo.com/advanced/response-cookies/
|
||||
|
||||
from fastapi import FastAPI, Response
|
||||
import fastapi.responses
|
||||
import asyncio
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/response_parameter") # $ routeSetup="/response_parameter"
|
||||
async def response_parameter(response: Response): # $ requestHandler
|
||||
response.set_cookie("key", "value") # $ CookieWrite CookieName="key" CookieValue="value"
|
||||
response.set_cookie(key="key", value="value") # $ CookieWrite CookieName="key" CookieValue="value"
|
||||
response.headers.append("Set-Cookie", "key2=value2") # $ CookieWrite CookieRawHeader="key2=value2"
|
||||
response.headers.append(key="Set-Cookie", value="key2=value2") # $ CookieWrite CookieRawHeader="key2=value2"
|
||||
response.headers["X-MyHeader"] = "header-value"
|
||||
response.status_code = 418
|
||||
return {"message": "response as parameter"} # $ HttpResponse mimetype=application/json responseBody=Dict
|
||||
|
||||
|
||||
@app.get("/resp_parameter") # $ routeSetup="/resp_parameter"
|
||||
async def resp_parameter(resp: Response): # $ requestHandler
|
||||
resp.status_code = 418
|
||||
return {"message": "resp as parameter"} # $ HttpResponse mimetype=application/json responseBody=Dict
|
||||
|
||||
|
||||
@app.get("/response_parameter_no_type") # $ routeSetup="/response_parameter_no_type"
|
||||
async def response_parameter_no_type(response): # $ requestHandler routedParameter=response
|
||||
# NOTE: This does in fact not work, since FastAPI relies on the type annotations,
|
||||
# and not on the name of the parameter
|
||||
response.status_code = 418
|
||||
return {"message": "response as parameter"} # $ HttpResponse mimetype=application/json responseBody=Dict
|
||||
|
||||
|
||||
class MyXmlResponse(fastapi.responses.Response):
|
||||
media_type = "application/xml"
|
||||
|
||||
|
||||
@app.get("/response_parameter_custom_type", response_class=MyXmlResponse) # $ routeSetup="/response_parameter_custom_type"
|
||||
async def response_parameter_custom_type(response: MyXmlResponse): # $ requestHandler
|
||||
# NOTE: This is a contrived example of using a wrong annotation for the response
|
||||
# parameter. It will be passed a `fastapi.responses.Response` value when handling an
|
||||
# incoming request, so NOT a `MyXmlResponse` value. Cookies/Headers are still
|
||||
# propagated to the final response though.
|
||||
print(type(response))
|
||||
assert type(response) == fastapi.responses.Response
|
||||
response.set_cookie("key", "value") # $ CookieWrite CookieName="key" CookieValue="value"
|
||||
response.headers["Custom-Response-Type"] = "yes, but only after function has run"
|
||||
xml_data = "<foo>FOO</foo>"
|
||||
return xml_data # $ HttpResponse responseBody=xml_data mimetype=application/xml
|
||||
|
||||
|
||||
# Direct response construction
|
||||
|
||||
# see https://fastapi.tiangolo.com/advanced/response-directly/
|
||||
# see https://fastapi.tiangolo.com/advanced/custom-response/
|
||||
|
||||
|
||||
|
||||
@app.get("/direct_response") # $ routeSetup="/direct_response"
|
||||
async def direct_response(): # $ requestHandler
|
||||
xml_data = "<foo>FOO</foo>"
|
||||
resp = fastapi.responses.Response(xml_data, 200, None, "application/xml") # $ HttpResponse mimetype=application/xml responseBody=xml_data
|
||||
resp = fastapi.responses.Response(content=xml_data, media_type="application/xml") # $ HttpResponse mimetype=application/xml responseBody=xml_data
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=application/json responseBody=resp
|
||||
|
||||
|
||||
@app.get("/direct_response2", response_class=fastapi.responses.Response) # $ routeSetup="/direct_response2"
|
||||
async def direct_response2(): # $ requestHandler
|
||||
xml_data = "<foo>FOO</foo>"
|
||||
return xml_data # $ HttpResponse responseBody=xml_data
|
||||
|
||||
|
||||
@app.get("/my_xml_response") # $ routeSetup="/my_xml_response"
|
||||
async def my_xml_response(): # $ requestHandler
|
||||
xml_data = "<foo>FOO</foo>"
|
||||
resp = MyXmlResponse(content=xml_data) # $ HttpResponse mimetype=application/xml responseBody=xml_data
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=application/json responseBody=resp
|
||||
|
||||
|
||||
@app.get("/my_xml_response2", response_class=MyXmlResponse) # $ routeSetup="/my_xml_response2"
|
||||
async def my_xml_response2(): # $ requestHandler
|
||||
xml_data = "<foo>FOO</foo>"
|
||||
return xml_data # $ HttpResponse responseBody=xml_data mimetype=application/xml
|
||||
|
||||
|
||||
@app.get("/html_response") # $ routeSetup="/html_response"
|
||||
async def html_response(): # $ requestHandler
|
||||
hello_world = "<h1>Hello World!</h1>"
|
||||
resp = fastapi.responses.HTMLResponse(hello_world) # $ HttpResponse mimetype=text/html responseBody=hello_world
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=application/json responseBody=resp
|
||||
|
||||
|
||||
@app.get("/html_response2", response_class=fastapi.responses.HTMLResponse) # $ routeSetup="/html_response2"
|
||||
async def html_response2(): # $ requestHandler
|
||||
hello_world = "<h1>Hello World!</h1>"
|
||||
return hello_world # $ HttpResponse responseBody=hello_world mimetype=text/html
|
||||
|
||||
|
||||
@app.get("/redirect") # $ routeSetup="/redirect"
|
||||
async def redirect(): # $ requestHandler
|
||||
next = "https://www.example.com"
|
||||
resp = fastapi.responses.RedirectResponse(next) # $ HttpResponse HttpRedirectResponse redirectLocation=next
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=application/json responseBody=resp
|
||||
|
||||
|
||||
@app.get("/redirect2", response_class=fastapi.responses.RedirectResponse) # $ routeSetup="/redirect2"
|
||||
async def redirect2(): # $ requestHandler
|
||||
next = "https://www.example.com"
|
||||
return next # $ HttpResponse HttpRedirectResponse redirectLocation=next
|
||||
|
||||
|
||||
@app.get("/streaming_response") # $ routeSetup="/streaming_response"
|
||||
async def streaming_response(): # $ requestHandler
|
||||
# You can test this with curl:
|
||||
# curl --no-buffer http://127.0.0.1:8000/streaming_response
|
||||
async def content():
|
||||
yield b"Hello "
|
||||
await asyncio.sleep(0.5)
|
||||
yield b"World"
|
||||
await asyncio.sleep(0.5)
|
||||
yield b"!"
|
||||
|
||||
resp = fastapi.responses.StreamingResponse(content()) # $ HttpResponse responseBody=content()
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=application/json responseBody=resp
|
||||
|
||||
|
||||
# setting `response_class` to `StreamingResponse` does not seem to work
|
||||
# so no such example here
|
||||
|
||||
|
||||
@app.get("/file_response") # $ routeSetup="/file_response"
|
||||
async def file_response(): # $ requestHandler
|
||||
# has internal dependency on PyPI package `aiofiles`
|
||||
# will guess MIME type from file extension
|
||||
|
||||
# We don't really have any good QL modeling of passing a file-path, whose content
|
||||
# will be returned as part of the response... so will leave this as a TODO for now.
|
||||
resp = fastapi.responses.FileResponse(__file__) # $ HttpResponse
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=application/json responseBody=resp
|
||||
|
||||
|
||||
@app.get("/file_response2", response_class=fastapi.responses.FileResponse) # $ routeSetup="/file_response2"
|
||||
async def file_response2(): # $ requestHandler
|
||||
return __file__ # $ HttpResponse
|
||||
33
python/ql/test/library-tests/frameworks/fastapi/router.py
Normal file
33
python/ql/test/library-tests/frameworks/fastapi/router.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# like blueprints in Flask
|
||||
# see https://fastapi.tiangolo.com/tutorial/bigger-applications/
|
||||
|
||||
from fastapi import APIRouter, FastAPI
|
||||
|
||||
|
||||
inner_router = APIRouter()
|
||||
|
||||
@inner_router.get("/foo") # $ routeSetup="/foo"
|
||||
async def root(): # $ requestHandler
|
||||
return {"msg": "inner_router /foo"} # $ HttpResponse
|
||||
|
||||
outer_router = APIRouter()
|
||||
outer_router.include_router(inner_router, prefix="/inner")
|
||||
|
||||
|
||||
items_router = APIRouter(
|
||||
prefix="/items",
|
||||
tags=["items"],
|
||||
)
|
||||
|
||||
|
||||
@items_router.get("/") # $ routeSetup="/"
|
||||
async def items(): # $ requestHandler
|
||||
return {"msg": "items_router /"} # $ HttpResponse
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.include_router(outer_router, prefix="/outer")
|
||||
app.include_router(items_router)
|
||||
|
||||
# see basic.py for instructions for how to run this code.
|
||||
189
python/ql/test/library-tests/frameworks/fastapi/taint_test.py
Normal file
189
python/ql/test/library-tests/frameworks/fastapi/taint_test.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# --- to make things runable ---
|
||||
|
||||
ensure_tainted = ensure_not_tainted = print
|
||||
|
||||
# --- real code ---
|
||||
|
||||
from fastapi import FastAPI
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Foo(BaseModel):
|
||||
foo: str
|
||||
|
||||
|
||||
class MyComplexModel(BaseModel):
|
||||
field: str
|
||||
main_foo: Foo
|
||||
other_foos: List[Foo]
|
||||
nested_foos: List[List[Foo]]
|
||||
|
||||
|
||||
@app.post("/test_taint/{name}/{number}") # $ routeSetup="/test_taint/{name}/{number}"
|
||||
async def test_taint(name : str, number : int, also_input: MyComplexModel): # $ requestHandler routedParameter=name routedParameter=number routedParameter=also_input
|
||||
ensure_tainted(
|
||||
name, # $ tainted
|
||||
number, # $ tainted
|
||||
|
||||
also_input, # $ tainted
|
||||
also_input.field, # $ tainted
|
||||
|
||||
also_input.main_foo, # $ tainted
|
||||
also_input.main_foo.foo, # $ tainted
|
||||
|
||||
also_input.other_foos, # $ tainted
|
||||
also_input.other_foos[0], # $ tainted
|
||||
also_input.other_foos[0].foo, # $ tainted
|
||||
[f.foo for f in also_input.other_foos], # $ MISSING: tainted
|
||||
|
||||
also_input.nested_foos, # $ tainted
|
||||
also_input.nested_foos[0], # $ tainted
|
||||
also_input.nested_foos[0][0], # $ tainted
|
||||
also_input.nested_foos[0][0].foo, # $ tainted
|
||||
)
|
||||
|
||||
other_foos = also_input.other_foos
|
||||
|
||||
ensure_tainted(
|
||||
other_foos, # $ tainted
|
||||
other_foos[0], # $ tainted
|
||||
other_foos[0].foo, # $ tainted
|
||||
[f.foo for f in other_foos], # $ MISSING: tainted
|
||||
)
|
||||
|
||||
return "ok" # $ HttpResponse
|
||||
|
||||
|
||||
# --- body ---
|
||||
# see https://fastapi.tiangolo.com/tutorial/body-multiple-params/
|
||||
|
||||
from fastapi import Body
|
||||
|
||||
# request is made such as `/will-be-query-param?name=foo`
|
||||
@app.post("/will-be-query-param") # $ routeSetup="/will-be-query-param"
|
||||
async def will_be_query_param(name: str): # $ requestHandler routedParameter=name
|
||||
ensure_tainted(name) # $ tainted
|
||||
return "ok" # $ HttpResponse
|
||||
|
||||
# with the `= Body(...)` "annotation" FastAPI will know to transmit `name` as part of
|
||||
# the HTTP post body
|
||||
@app.post("/will-not-be-query-param") # $ routeSetup="/will-not-be-query-param"
|
||||
async def will_not_be_query_param(name: str = Body("foo", media_type="text/plain")): # $ requestHandler routedParameter=name
|
||||
ensure_tainted(name) # $ tainted
|
||||
return "ok" # $ HttpResponse
|
||||
|
||||
|
||||
# --- form data ---
|
||||
# see https://fastapi.tiangolo.com/tutorial/request-forms/
|
||||
|
||||
from fastapi import Form
|
||||
|
||||
@app.post("/form-example") # $ routeSetup="/form-example"
|
||||
async def form_example(username: str = Form(None)): # $ requestHandler routedParameter=username
|
||||
ensure_tainted(username) # $ tainted
|
||||
return "ok" # $ HttpResponse
|
||||
|
||||
|
||||
# --- HTTP headers ---
|
||||
# see https://fastapi.tiangolo.com/tutorial/header-params/
|
||||
|
||||
from fastapi import Header
|
||||
|
||||
@app.get("/header-example") # $ routeSetup="/header-example"
|
||||
async def header_example(user_agent: Optional[str] = Header(None)): # $ requestHandler routedParameter=user_agent
|
||||
ensure_tainted(user_agent) # $ tainted
|
||||
return "ok" # $ HttpResponse
|
||||
|
||||
|
||||
# --- file upload ---
|
||||
# see https://fastapi.tiangolo.com/tutorial/request-files/
|
||||
# see https://fastapi.tiangolo.com/tutorial/request-files/#uploadfile
|
||||
|
||||
from fastapi import File, UploadFile
|
||||
|
||||
@app.post("/file-upload") # $ routeSetup="/file-upload"
|
||||
async def file_upload(f1: bytes = File(None), f2: UploadFile = File(None)): # $ requestHandler routedParameter=f1 routedParameter=f2
|
||||
ensure_tainted(
|
||||
f1, # $ tainted
|
||||
|
||||
f2, # $ tainted
|
||||
f2.filename, # $ MISSING: tainted
|
||||
f2.content_type, # $ MISSING: tainted
|
||||
f2.file, # $ MISSING: tainted
|
||||
f2.file.read(), # $ MISSING: tainted
|
||||
f2.file.readline(), # $ MISSING: tainted
|
||||
f2.file.readlines(), # $ MISSING: tainted
|
||||
await f2.read(), # $ MISSING: tainted
|
||||
)
|
||||
return "ok" # $ HttpResponse
|
||||
|
||||
# --- WebSocket ---
|
||||
|
||||
import starlette.websockets
|
||||
from fastapi import WebSocket
|
||||
|
||||
|
||||
assert WebSocket == starlette.websockets.WebSocket
|
||||
|
||||
|
||||
@app.websocket("/ws") # $ routeSetup="/ws"
|
||||
async def websocket_test(websocket: WebSocket): # $ requestHandler routedParameter=websocket
|
||||
await websocket.accept()
|
||||
|
||||
ensure_tainted(
|
||||
websocket, # $ tainted
|
||||
|
||||
websocket.url, # $ tainted
|
||||
|
||||
websocket.url.netloc, # $ tainted
|
||||
websocket.url.path, # $ tainted
|
||||
websocket.url.query, # $ tainted
|
||||
websocket.url.fragment, # $ tainted
|
||||
websocket.url.username, # $ tainted
|
||||
websocket.url.password, # $ tainted
|
||||
websocket.url.hostname, # $ tainted
|
||||
websocket.url.port, # $ tainted
|
||||
|
||||
websocket.url.components, # $ tainted
|
||||
websocket.url.components.netloc, # $ tainted
|
||||
websocket.url.components.path, # $ tainted
|
||||
websocket.url.components.query, # $ tainted
|
||||
websocket.url.components.fragment, # $ tainted
|
||||
websocket.url.components.username, # $ tainted
|
||||
websocket.url.components.password, # $ tainted
|
||||
websocket.url.components.hostname, # $ tainted
|
||||
websocket.url.components.port, # $ tainted
|
||||
|
||||
websocket.headers, # $ tainted
|
||||
websocket.headers["key"], # $ tainted
|
||||
|
||||
websocket.query_params, # $ tainted
|
||||
websocket.query_params["key"], # $ tainted
|
||||
|
||||
websocket.cookies, # $ tainted
|
||||
websocket.cookies["key"], # $ tainted
|
||||
|
||||
await websocket.receive(), # $ tainted
|
||||
await websocket.receive_bytes(), # $ tainted
|
||||
await websocket.receive_text(), # $ tainted
|
||||
await websocket.receive_json(), # $ tainted
|
||||
)
|
||||
|
||||
# scheme seems very unlikely to give interesting results, but very likely to give FPs.
|
||||
ensure_not_tainted(
|
||||
websocket.url.scheme,
|
||||
websocket.url.components.scheme,
|
||||
)
|
||||
|
||||
async for data in websocket.iter_bytes():
|
||||
ensure_tainted(data) # $ tainted
|
||||
|
||||
async for data in websocket.iter_text():
|
||||
ensure_tainted(data) # $ tainted
|
||||
|
||||
async for data in websocket.iter_json():
|
||||
ensure_tainted(data) # $ tainted
|
||||
@@ -0,0 +1,7 @@
|
||||
from flask import send_from_directory, send_file
|
||||
|
||||
send_from_directory("dir", "file") # $ getAPathArgument="dir" getAPathArgument="file"
|
||||
send_from_directory(directory="dir", filename="file") # $ getAPathArgument="dir" getAPathArgument="file"
|
||||
|
||||
send_file("file") # $ getAPathArgument="file"
|
||||
send_file(filename_or_fp="file") # $ getAPathArgument="file"
|
||||
@@ -105,8 +105,8 @@ def bp1_example(foo): # $ requestHandler routedParameter=foo
|
||||
|
||||
app.register_blueprint(bp1) # by default, URLs of blueprints are not prefixed
|
||||
|
||||
|
||||
bp2 = flask.Blueprint("bp2", __name__)
|
||||
import flask.blueprints
|
||||
bp2 = flask.blueprints.Blueprint("bp2", __name__)
|
||||
|
||||
@bp2.route("/example") # $ routeSetup="/example"
|
||||
def bp2_example(): # $ requestHandler
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
from flask import Flask, request, send_from_directory, send_file
|
||||
from flask import Flask, request
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/save-uploaded-file") # $routeSetup="/save-uploaded-file"
|
||||
def test_taint(): # $requestHandler
|
||||
request.files['key'].save("path") # $ getAPathArgument="path"
|
||||
|
||||
|
||||
@app.route("/path-injection") # $routeSetup="/path-injection"
|
||||
def test_path(): # $requestHandler
|
||||
|
||||
send_from_directory("filepath","file") # $ getAPathArgument="filepath" getAPathArgument="file"
|
||||
send_file("file") # $ getAPathArgument="file"
|
||||
|
||||
send_from_directory(directory="filepath","file") # $ getAPathArgument="filepath" getAPathArgument="file"
|
||||
send_from_directory(filename="filepath","file") # $ getAPathArgument="filepath" getAPathArgument="file"
|
||||
send_file(filename_or_fp="file") # $ getAPathArgument="file"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
|
||||
class DedicatedResponseTest extends HttpServerHttpResponseTest {
|
||||
DedicatedResponseTest() { file.getShortName() = "response_test.py" }
|
||||
}
|
||||
|
||||
class OtherResponseTest extends HttpServerHttpResponseTest {
|
||||
OtherResponseTest() { not this instanceof DedicatedResponseTest }
|
||||
|
||||
override string getARelevantTag() { result = "HttpResponse" }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
argumentToEnsureNotTaintedNotMarkedAsSpurious
|
||||
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
|
||||
failures
|
||||
@@ -0,0 +1 @@
|
||||
import experimental.meta.InlineTaintTest
|
||||
58
python/ql/test/library-tests/frameworks/flask_admin/test.py
Normal file
58
python/ql/test/library-tests/frameworks/flask_admin/test.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from flask import Flask, redirect
|
||||
from flask.views import MethodView
|
||||
import flask_admin
|
||||
|
||||
ensure_tainted = ensure_not_tainted = print
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# unknown at least for our current analysis
|
||||
foo = "'/foo'"
|
||||
UNKNOWN_ROUTE = eval(foo) # $ getCode=foo
|
||||
|
||||
|
||||
class ExampleClass(flask_admin.BaseView):
|
||||
@flask_admin.expose('/') # $ routeSetup="/"
|
||||
def foo(self): # $ requestHandler
|
||||
return "foo" # $ HttpResponse
|
||||
|
||||
@flask_admin.expose(url='/bar/<arg>') # $ routeSetup="/bar/<arg>"
|
||||
def bar(self, arg): # $ requestHandler routedParameter=arg
|
||||
ensure_tainted(arg) # $ tainted
|
||||
return "bar: " + arg # $ HttpResponse
|
||||
|
||||
@flask_admin.expose_plugview("/flask-class") # $ routeSetup="/flask-class"
|
||||
@flask_admin.expose_plugview(url="/flask-class/<arg>") # $ routeSetup="/flask-class/<arg>"
|
||||
class Nested(MethodView):
|
||||
def get(self, cls, arg="default"): # $ requestHandler routedParameter=arg
|
||||
assert isinstance(cls, ExampleClass)
|
||||
ensure_tainted(arg) # $ tainted
|
||||
ensure_not_tainted(cls)
|
||||
return "GET: " + arg # $ HttpResponse
|
||||
|
||||
def post(self, cls, arg): # $ requestHandler routedParameter=arg
|
||||
assert isinstance(cls, ExampleClass)
|
||||
ensure_tainted(arg) # $ tainted
|
||||
ensure_not_tainted(cls)
|
||||
return "POST: " + arg # $ HttpResponse
|
||||
|
||||
@flask_admin.expose_plugview(UNKNOWN_ROUTE) # $ routeSetup
|
||||
class WithUnknownRoute(MethodView):
|
||||
def get(self, cls, maybeRouted): # $ requestHandler routedParameter=maybeRouted
|
||||
ensure_tainted(maybeRouted) # $ tainted
|
||||
ensure_not_tainted(cls)
|
||||
return "ok" # $ HttpResponse
|
||||
|
||||
|
||||
@app.route('/') # $ routeSetup="/"
|
||||
def index(): # $ requestHandler
|
||||
return redirect('/admin') # $ HttpRedirectResponse HttpResponse redirectLocation='/admin'
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
admin = flask_admin.Admin(app, name="Some Admin Interface")
|
||||
admin.add_view(ExampleClass())
|
||||
|
||||
print(app.url_map)
|
||||
app.run(debug=True)
|
||||
@@ -12,7 +12,7 @@ db = SQLAlchemy(app)
|
||||
# - https://github.com/pallets/flask-sqlalchemy/blob/931ec00d1e27f51508e05706eef41cc4419a0b32/src/flask_sqlalchemy/__init__.py#L765
|
||||
# - https://github.com/pallets/flask-sqlalchemy/blob/931ec00d1e27f51508e05706eef41cc4419a0b32/src/flask_sqlalchemy/__init__.py#L99-L109
|
||||
|
||||
assert str(type(db.text("Foo"))) == "<class 'sqlalchemy.sql.elements.TextClause'>"
|
||||
assert str(type(db.text("Foo"))) == "<class 'sqlalchemy.sql.elements.TextClause'>" # $ constructedSql="Foo"
|
||||
|
||||
# also has engine/session instantiated
|
||||
|
||||
@@ -44,8 +44,8 @@ assert result.fetchall() == [("Foo",)]
|
||||
|
||||
|
||||
# text
|
||||
t = db.text("foo")
|
||||
t = db.text("foo") # $ constructedSql="foo"
|
||||
assert isinstance(t, sqlalchemy.sql.expression.TextClause)
|
||||
|
||||
t = db.text(text="foo")
|
||||
t = db.text(text="foo") # $ constructedSql="foo"
|
||||
assert isinstance(t, sqlalchemy.sql.expression.TextClause)
|
||||
|
||||
1
python/ql/test/library-tests/frameworks/rest_framework/.gitignore
vendored
Normal file
1
python/ql/test/library-tests/frameworks/rest_framework/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
db.sqlite3
|
||||
@@ -0,0 +1,2 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
@@ -0,0 +1,3 @@
|
||||
argumentToEnsureNotTaintedNotMarkedAsSpurious
|
||||
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
|
||||
failures
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user