mirror of
https://github.com/github/codeql.git
synced 2025-12-22 11:46:32 +01:00
Merge branch main into rc/3.6
This commit is contained in:
@@ -136,6 +136,9 @@ module API {
|
||||
result = this.getASuccessor(Label::keywordParameter(name))
|
||||
}
|
||||
|
||||
/** Gets the node representing the self parameter */
|
||||
Node getSelfParameter() { result = this.getASuccessor(Label::selfParameter()) }
|
||||
|
||||
/**
|
||||
* Gets the number of parameters of the function represented by this node.
|
||||
*/
|
||||
@@ -321,6 +324,12 @@ module API {
|
||||
/** Gets the API node for a parameter of this invocation. */
|
||||
Node getAParameter() { result = this.getParameter(_) }
|
||||
|
||||
/** Gets the object that this method-call is being called on, if this is a method-call */
|
||||
Node getSelfParameter() {
|
||||
result.getARhs() = this.(DataFlow::MethodCallNode).getObject() and
|
||||
result = callee.getSelfParameter()
|
||||
}
|
||||
|
||||
/** Gets the API node for the keyword parameter `name` of this invocation. */
|
||||
Node getKeywordParameter(string name) {
|
||||
result = callee.getKeywordParameter(name) and
|
||||
@@ -345,6 +354,14 @@ module API {
|
||||
result = callee.getReturn() and
|
||||
result.getAnImmediateUse() = this
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of positional arguments of this call.
|
||||
*
|
||||
* Note: This is used for `WithArity[<n>]` in modeling-as-data, where we thought
|
||||
* including keyword arguments didn't make much sense.
|
||||
*/
|
||||
int getNumArgument() { result = count(this.getArg(_)) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -589,15 +606,24 @@ module API {
|
||||
exists(DataFlow::Node def, PY::CallableExpr fn |
|
||||
rhs(base, def) and fn = trackDefNode(def).asExpr()
|
||||
|
|
||||
exists(int i |
|
||||
lbl = Label::parameter(i) and
|
||||
exists(int i, int offset |
|
||||
if exists(PY::Parameter p | p = fn.getInnerScope().getAnArg() and p.isSelf())
|
||||
then offset = 1
|
||||
else offset = 0
|
||||
|
|
||||
lbl = Label::parameter(i - offset) and
|
||||
ref.asExpr() = fn.getInnerScope().getArg(i)
|
||||
)
|
||||
or
|
||||
exists(string name |
|
||||
exists(string name, PY::Parameter param |
|
||||
lbl = Label::keywordParameter(name) and
|
||||
ref.asExpr() = fn.getInnerScope().getArgByName(name)
|
||||
param = fn.getInnerScope().getArgByName(name) and
|
||||
not param.isSelf() and
|
||||
ref.asExpr() = param
|
||||
)
|
||||
or
|
||||
lbl = Label::selfParameter() and
|
||||
ref.asExpr() = any(PY::Parameter p | p = fn.getInnerScope().getAnArg() and p.isSelf())
|
||||
)
|
||||
or
|
||||
// Built-ins, treated as members of the module `builtins`
|
||||
@@ -664,6 +690,9 @@ module API {
|
||||
exists(string name | lbl = Label::keywordParameter(name) |
|
||||
arg = pred.getACall().getArgByName(name)
|
||||
)
|
||||
or
|
||||
lbl = Label::selfParameter() and
|
||||
arg = pred.getACall().(DataFlow::MethodCallNode).getObject()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -780,6 +809,7 @@ module API {
|
||||
or
|
||||
exists(any(PY::Function f).getArgByName(name))
|
||||
} or
|
||||
MkLabelSelfParameter() or
|
||||
MkLabelReturn() or
|
||||
MkLabelSubclass() or
|
||||
MkLabelAwait()
|
||||
@@ -837,6 +867,11 @@ module API {
|
||||
string getName() { result = name }
|
||||
}
|
||||
|
||||
/** A label for the self parameter. */
|
||||
class LabelSelfParameter extends ApiLabel, MkLabelSelfParameter {
|
||||
override string toString() { result = "getSelfParameter()" }
|
||||
}
|
||||
|
||||
/** A label that gets the return value of a function. */
|
||||
class LabelReturn extends ApiLabel, MkLabelReturn {
|
||||
override string toString() { result = "getReturn()" }
|
||||
@@ -876,6 +911,9 @@ module API {
|
||||
/** Gets the `parameter` edge label for the keyword parameter `name`. */
|
||||
LabelKeywordParameter keywordParameter(string name) { result.getName() = name }
|
||||
|
||||
/** Gets the edge label for the self parameter. */
|
||||
LabelSelfParameter selfParameter() { any() }
|
||||
|
||||
/** Gets the `return` edge label. */
|
||||
LabelReturn return() { any() }
|
||||
|
||||
|
||||
@@ -1211,38 +1211,5 @@ module Cryptography {
|
||||
}
|
||||
}
|
||||
|
||||
import semmle.python.concepts.CryptoAlgorithms
|
||||
|
||||
/**
|
||||
* A data-flow node that is an application of a cryptographic algorithm. For example,
|
||||
* encryption, decryption, signature-validation.
|
||||
*
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `CryptographicOperation::Range` instead.
|
||||
*/
|
||||
class CryptographicOperation extends DataFlow::Node instanceof CryptographicOperation::Range {
|
||||
/** Gets the algorithm used, if it matches a known `CryptographicAlgorithm`. */
|
||||
CryptographicAlgorithm getAlgorithm() { result = super.getAlgorithm() }
|
||||
|
||||
/** Gets an input the algorithm is used on, for example the plain text input to be encrypted. */
|
||||
DataFlow::Node getAnInput() { result = super.getAnInput() }
|
||||
}
|
||||
|
||||
/** Provides classes for modeling new applications of a cryptographic algorithms. */
|
||||
module CryptographicOperation {
|
||||
/**
|
||||
* A data-flow node that is an application of a cryptographic algorithm. For example,
|
||||
* encryption, decryption, signature-validation.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `CryptographicOperation` instead.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node {
|
||||
/** Gets the algorithm used, if it matches a known `CryptographicAlgorithm`. */
|
||||
abstract CryptographicAlgorithm getAlgorithm();
|
||||
|
||||
/** Gets an input the algorithm is used on, for example the plain text input to be encrypted. */
|
||||
abstract DataFlow::Node getAnInput();
|
||||
}
|
||||
}
|
||||
import semmle.python.internal.ConceptsShared::Cryptography
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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.data.ModelsAsData
|
||||
private import semmle.python.frameworks.Dill
|
||||
private import semmle.python.frameworks.Django
|
||||
private import semmle.python.frameworks.Fabric
|
||||
|
||||
@@ -81,6 +81,9 @@ class EncryptionAlgorithm extends MkEncryptionAlgorithm, CryptographicAlgorithm
|
||||
override string getName() { result = name }
|
||||
|
||||
override predicate isWeak() { isWeak = true }
|
||||
|
||||
/** Holds if this algorithm is a stream cipher. */
|
||||
predicate isStreamCipher() { isStreamCipher(name) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -67,6 +67,6 @@ predicate isStrongPasswordHashingAlgorithm(string name) {
|
||||
predicate isWeakPasswordHashingAlgorithm(string name) { name = "EVPKDF" }
|
||||
|
||||
/**
|
||||
* Holds if `name` corresponds to a weak block cipher mode of operation.
|
||||
* Holds if `name` corresponds to a stream cipher.
|
||||
*/
|
||||
predicate isWeakBlockMode(string name) { name = "ECB" }
|
||||
predicate isStreamCipher(string name) { name = ["CHACHA", "RC4", "ARC4", "ARCFOUR", "RABBIT"] }
|
||||
|
||||
@@ -3854,16 +3854,11 @@ class PathNode extends TPathNode {
|
||||
/** Gets the associated configuration. */
|
||||
Configuration getConfiguration() { none() }
|
||||
|
||||
private PathNode getASuccessorIfHidden() {
|
||||
this.(PathNodeImpl).isHidden() and
|
||||
result = this.(PathNodeImpl).getASuccessorImpl()
|
||||
}
|
||||
|
||||
/** Gets a successor of this node, if any. */
|
||||
final PathNode getASuccessor() {
|
||||
result = this.(PathNodeImpl).getASuccessorImpl().getASuccessorIfHidden*() and
|
||||
not this.(PathNodeImpl).isHidden() and
|
||||
not result.(PathNodeImpl).isHidden()
|
||||
result = this.(PathNodeImpl).getANonHiddenSuccessor() and
|
||||
reach(this) and
|
||||
reach(result)
|
||||
}
|
||||
|
||||
/** Holds if this node is a source. */
|
||||
@@ -3871,7 +3866,18 @@ class PathNode extends TPathNode {
|
||||
}
|
||||
|
||||
abstract private class PathNodeImpl extends PathNode {
|
||||
abstract PathNode getASuccessorImpl();
|
||||
abstract PathNodeImpl getASuccessorImpl();
|
||||
|
||||
private PathNodeImpl getASuccessorIfHidden() {
|
||||
this.isHidden() and
|
||||
result = this.getASuccessorImpl()
|
||||
}
|
||||
|
||||
final PathNodeImpl getANonHiddenSuccessor() {
|
||||
result = this.getASuccessorImpl().getASuccessorIfHidden*() and
|
||||
not this.isHidden() and
|
||||
not result.isHidden()
|
||||
}
|
||||
|
||||
abstract NodeEx getNodeEx();
|
||||
|
||||
@@ -3914,15 +3920,17 @@ abstract private class PathNodeImpl extends PathNode {
|
||||
}
|
||||
|
||||
/** Holds if `n` can reach a sink. */
|
||||
private predicate directReach(PathNode n) {
|
||||
n instanceof PathNodeSink or directReach(n.getASuccessor())
|
||||
private predicate directReach(PathNodeImpl n) {
|
||||
n instanceof PathNodeSink or directReach(n.getANonHiddenSuccessor())
|
||||
}
|
||||
|
||||
/** Holds if `n` can reach a sink or is used in a subpath that can reach a sink. */
|
||||
private predicate reach(PathNode n) { directReach(n) or Subpaths::retReach(n) }
|
||||
|
||||
/** Holds if `n1.getASuccessor() = n2` and `n2` can reach a sink. */
|
||||
private predicate pathSucc(PathNode n1, PathNode n2) { n1.getASuccessor() = n2 and directReach(n2) }
|
||||
private predicate pathSucc(PathNodeImpl n1, PathNode n2) {
|
||||
n1.getANonHiddenSuccessor() = n2 and directReach(n2)
|
||||
}
|
||||
|
||||
private predicate pathSuccPlus(PathNode n1, PathNode n2) = fastTC(pathSucc/2)(n1, n2)
|
||||
|
||||
@@ -3931,7 +3939,7 @@ private predicate pathSuccPlus(PathNode n1, PathNode n2) = fastTC(pathSucc/2)(n1
|
||||
*/
|
||||
module PathGraph {
|
||||
/** Holds if `(a,b)` is an edge in the graph of data flow path explanations. */
|
||||
query predicate edges(PathNode a, PathNode b) { a.getASuccessor() = b and reach(a) and reach(b) }
|
||||
query predicate edges(PathNode a, PathNode b) { a.getASuccessor() = b }
|
||||
|
||||
/** Holds if `n` is a node in the graph of data flow path explanations. */
|
||||
query predicate nodes(PathNode n, string key, string val) {
|
||||
@@ -4049,7 +4057,7 @@ private class PathNodeSink extends PathNodeImpl, TPathNodeSink {
|
||||
|
||||
override Configuration getConfiguration() { result = config }
|
||||
|
||||
override PathNode getASuccessorImpl() { none() }
|
||||
override PathNodeImpl getASuccessorImpl() { none() }
|
||||
|
||||
override predicate isSource() { sourceNode(node, state, config) }
|
||||
}
|
||||
@@ -4365,8 +4373,8 @@ private module Subpaths {
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate hasSuccessor(PathNode pred, PathNodeMid succ, NodeEx succNode) {
|
||||
succ = pred.getASuccessor() and
|
||||
private predicate hasSuccessor(PathNodeImpl pred, PathNodeMid succ, NodeEx succNode) {
|
||||
succ = pred.getANonHiddenSuccessor() and
|
||||
succNode = succ.getNodeEx()
|
||||
}
|
||||
|
||||
@@ -4375,9 +4383,9 @@ private module Subpaths {
|
||||
* a subpath between `par` and `ret` with the connecting edges `arg -> par` and
|
||||
* `ret -> out` is summarized as the edge `arg -> out`.
|
||||
*/
|
||||
predicate subpaths(PathNode arg, PathNodeImpl par, PathNodeImpl ret, PathNode out) {
|
||||
predicate subpaths(PathNodeImpl arg, PathNodeImpl par, PathNodeImpl ret, PathNode out) {
|
||||
exists(ParamNodeEx p, NodeEx o, FlowState sout, AccessPath apout, PathNodeMid out0 |
|
||||
pragma[only_bind_into](arg).getASuccessor() = pragma[only_bind_into](out0) and
|
||||
pragma[only_bind_into](arg).getANonHiddenSuccessor() = pragma[only_bind_into](out0) and
|
||||
subpaths03(pragma[only_bind_into](arg), p, localStepToHidden*(ret), o, sout, apout) and
|
||||
hasSuccessor(pragma[only_bind_into](arg), par, p) and
|
||||
not ret.isHidden() and
|
||||
@@ -4390,12 +4398,12 @@ private module Subpaths {
|
||||
/**
|
||||
* Holds if `n` can reach a return node in a summarized subpath that can reach a sink.
|
||||
*/
|
||||
predicate retReach(PathNode n) {
|
||||
predicate retReach(PathNodeImpl n) {
|
||||
exists(PathNode out | subpaths(_, _, n, out) | directReach(out) or retReach(out))
|
||||
or
|
||||
exists(PathNode mid |
|
||||
exists(PathNodeImpl mid |
|
||||
retReach(mid) and
|
||||
n.getASuccessor() = mid and
|
||||
n.getANonHiddenSuccessor() = mid and
|
||||
not subpaths(_, mid, _, _)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3854,16 +3854,11 @@ class PathNode extends TPathNode {
|
||||
/** Gets the associated configuration. */
|
||||
Configuration getConfiguration() { none() }
|
||||
|
||||
private PathNode getASuccessorIfHidden() {
|
||||
this.(PathNodeImpl).isHidden() and
|
||||
result = this.(PathNodeImpl).getASuccessorImpl()
|
||||
}
|
||||
|
||||
/** Gets a successor of this node, if any. */
|
||||
final PathNode getASuccessor() {
|
||||
result = this.(PathNodeImpl).getASuccessorImpl().getASuccessorIfHidden*() and
|
||||
not this.(PathNodeImpl).isHidden() and
|
||||
not result.(PathNodeImpl).isHidden()
|
||||
result = this.(PathNodeImpl).getANonHiddenSuccessor() and
|
||||
reach(this) and
|
||||
reach(result)
|
||||
}
|
||||
|
||||
/** Holds if this node is a source. */
|
||||
@@ -3871,7 +3866,18 @@ class PathNode extends TPathNode {
|
||||
}
|
||||
|
||||
abstract private class PathNodeImpl extends PathNode {
|
||||
abstract PathNode getASuccessorImpl();
|
||||
abstract PathNodeImpl getASuccessorImpl();
|
||||
|
||||
private PathNodeImpl getASuccessorIfHidden() {
|
||||
this.isHidden() and
|
||||
result = this.getASuccessorImpl()
|
||||
}
|
||||
|
||||
final PathNodeImpl getANonHiddenSuccessor() {
|
||||
result = this.getASuccessorImpl().getASuccessorIfHidden*() and
|
||||
not this.isHidden() and
|
||||
not result.isHidden()
|
||||
}
|
||||
|
||||
abstract NodeEx getNodeEx();
|
||||
|
||||
@@ -3914,15 +3920,17 @@ abstract private class PathNodeImpl extends PathNode {
|
||||
}
|
||||
|
||||
/** Holds if `n` can reach a sink. */
|
||||
private predicate directReach(PathNode n) {
|
||||
n instanceof PathNodeSink or directReach(n.getASuccessor())
|
||||
private predicate directReach(PathNodeImpl n) {
|
||||
n instanceof PathNodeSink or directReach(n.getANonHiddenSuccessor())
|
||||
}
|
||||
|
||||
/** Holds if `n` can reach a sink or is used in a subpath that can reach a sink. */
|
||||
private predicate reach(PathNode n) { directReach(n) or Subpaths::retReach(n) }
|
||||
|
||||
/** Holds if `n1.getASuccessor() = n2` and `n2` can reach a sink. */
|
||||
private predicate pathSucc(PathNode n1, PathNode n2) { n1.getASuccessor() = n2 and directReach(n2) }
|
||||
private predicate pathSucc(PathNodeImpl n1, PathNode n2) {
|
||||
n1.getANonHiddenSuccessor() = n2 and directReach(n2)
|
||||
}
|
||||
|
||||
private predicate pathSuccPlus(PathNode n1, PathNode n2) = fastTC(pathSucc/2)(n1, n2)
|
||||
|
||||
@@ -3931,7 +3939,7 @@ private predicate pathSuccPlus(PathNode n1, PathNode n2) = fastTC(pathSucc/2)(n1
|
||||
*/
|
||||
module PathGraph {
|
||||
/** Holds if `(a,b)` is an edge in the graph of data flow path explanations. */
|
||||
query predicate edges(PathNode a, PathNode b) { a.getASuccessor() = b and reach(a) and reach(b) }
|
||||
query predicate edges(PathNode a, PathNode b) { a.getASuccessor() = b }
|
||||
|
||||
/** Holds if `n` is a node in the graph of data flow path explanations. */
|
||||
query predicate nodes(PathNode n, string key, string val) {
|
||||
@@ -4049,7 +4057,7 @@ private class PathNodeSink extends PathNodeImpl, TPathNodeSink {
|
||||
|
||||
override Configuration getConfiguration() { result = config }
|
||||
|
||||
override PathNode getASuccessorImpl() { none() }
|
||||
override PathNodeImpl getASuccessorImpl() { none() }
|
||||
|
||||
override predicate isSource() { sourceNode(node, state, config) }
|
||||
}
|
||||
@@ -4365,8 +4373,8 @@ private module Subpaths {
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate hasSuccessor(PathNode pred, PathNodeMid succ, NodeEx succNode) {
|
||||
succ = pred.getASuccessor() and
|
||||
private predicate hasSuccessor(PathNodeImpl pred, PathNodeMid succ, NodeEx succNode) {
|
||||
succ = pred.getANonHiddenSuccessor() and
|
||||
succNode = succ.getNodeEx()
|
||||
}
|
||||
|
||||
@@ -4375,9 +4383,9 @@ private module Subpaths {
|
||||
* a subpath between `par` and `ret` with the connecting edges `arg -> par` and
|
||||
* `ret -> out` is summarized as the edge `arg -> out`.
|
||||
*/
|
||||
predicate subpaths(PathNode arg, PathNodeImpl par, PathNodeImpl ret, PathNode out) {
|
||||
predicate subpaths(PathNodeImpl arg, PathNodeImpl par, PathNodeImpl ret, PathNode out) {
|
||||
exists(ParamNodeEx p, NodeEx o, FlowState sout, AccessPath apout, PathNodeMid out0 |
|
||||
pragma[only_bind_into](arg).getASuccessor() = pragma[only_bind_into](out0) and
|
||||
pragma[only_bind_into](arg).getANonHiddenSuccessor() = pragma[only_bind_into](out0) and
|
||||
subpaths03(pragma[only_bind_into](arg), p, localStepToHidden*(ret), o, sout, apout) and
|
||||
hasSuccessor(pragma[only_bind_into](arg), par, p) and
|
||||
not ret.isHidden() and
|
||||
@@ -4390,12 +4398,12 @@ private module Subpaths {
|
||||
/**
|
||||
* Holds if `n` can reach a return node in a summarized subpath that can reach a sink.
|
||||
*/
|
||||
predicate retReach(PathNode n) {
|
||||
predicate retReach(PathNodeImpl n) {
|
||||
exists(PathNode out | subpaths(_, _, n, out) | directReach(out) or retReach(out))
|
||||
or
|
||||
exists(PathNode mid |
|
||||
exists(PathNodeImpl mid |
|
||||
retReach(mid) and
|
||||
n.getASuccessor() = mid and
|
||||
n.getANonHiddenSuccessor() = mid and
|
||||
not subpaths(_, mid, _, _)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3854,16 +3854,11 @@ class PathNode extends TPathNode {
|
||||
/** Gets the associated configuration. */
|
||||
Configuration getConfiguration() { none() }
|
||||
|
||||
private PathNode getASuccessorIfHidden() {
|
||||
this.(PathNodeImpl).isHidden() and
|
||||
result = this.(PathNodeImpl).getASuccessorImpl()
|
||||
}
|
||||
|
||||
/** Gets a successor of this node, if any. */
|
||||
final PathNode getASuccessor() {
|
||||
result = this.(PathNodeImpl).getASuccessorImpl().getASuccessorIfHidden*() and
|
||||
not this.(PathNodeImpl).isHidden() and
|
||||
not result.(PathNodeImpl).isHidden()
|
||||
result = this.(PathNodeImpl).getANonHiddenSuccessor() and
|
||||
reach(this) and
|
||||
reach(result)
|
||||
}
|
||||
|
||||
/** Holds if this node is a source. */
|
||||
@@ -3871,7 +3866,18 @@ class PathNode extends TPathNode {
|
||||
}
|
||||
|
||||
abstract private class PathNodeImpl extends PathNode {
|
||||
abstract PathNode getASuccessorImpl();
|
||||
abstract PathNodeImpl getASuccessorImpl();
|
||||
|
||||
private PathNodeImpl getASuccessorIfHidden() {
|
||||
this.isHidden() and
|
||||
result = this.getASuccessorImpl()
|
||||
}
|
||||
|
||||
final PathNodeImpl getANonHiddenSuccessor() {
|
||||
result = this.getASuccessorImpl().getASuccessorIfHidden*() and
|
||||
not this.isHidden() and
|
||||
not result.isHidden()
|
||||
}
|
||||
|
||||
abstract NodeEx getNodeEx();
|
||||
|
||||
@@ -3914,15 +3920,17 @@ abstract private class PathNodeImpl extends PathNode {
|
||||
}
|
||||
|
||||
/** Holds if `n` can reach a sink. */
|
||||
private predicate directReach(PathNode n) {
|
||||
n instanceof PathNodeSink or directReach(n.getASuccessor())
|
||||
private predicate directReach(PathNodeImpl n) {
|
||||
n instanceof PathNodeSink or directReach(n.getANonHiddenSuccessor())
|
||||
}
|
||||
|
||||
/** Holds if `n` can reach a sink or is used in a subpath that can reach a sink. */
|
||||
private predicate reach(PathNode n) { directReach(n) or Subpaths::retReach(n) }
|
||||
|
||||
/** Holds if `n1.getASuccessor() = n2` and `n2` can reach a sink. */
|
||||
private predicate pathSucc(PathNode n1, PathNode n2) { n1.getASuccessor() = n2 and directReach(n2) }
|
||||
private predicate pathSucc(PathNodeImpl n1, PathNode n2) {
|
||||
n1.getANonHiddenSuccessor() = n2 and directReach(n2)
|
||||
}
|
||||
|
||||
private predicate pathSuccPlus(PathNode n1, PathNode n2) = fastTC(pathSucc/2)(n1, n2)
|
||||
|
||||
@@ -3931,7 +3939,7 @@ private predicate pathSuccPlus(PathNode n1, PathNode n2) = fastTC(pathSucc/2)(n1
|
||||
*/
|
||||
module PathGraph {
|
||||
/** Holds if `(a,b)` is an edge in the graph of data flow path explanations. */
|
||||
query predicate edges(PathNode a, PathNode b) { a.getASuccessor() = b and reach(a) and reach(b) }
|
||||
query predicate edges(PathNode a, PathNode b) { a.getASuccessor() = b }
|
||||
|
||||
/** Holds if `n` is a node in the graph of data flow path explanations. */
|
||||
query predicate nodes(PathNode n, string key, string val) {
|
||||
@@ -4049,7 +4057,7 @@ private class PathNodeSink extends PathNodeImpl, TPathNodeSink {
|
||||
|
||||
override Configuration getConfiguration() { result = config }
|
||||
|
||||
override PathNode getASuccessorImpl() { none() }
|
||||
override PathNodeImpl getASuccessorImpl() { none() }
|
||||
|
||||
override predicate isSource() { sourceNode(node, state, config) }
|
||||
}
|
||||
@@ -4365,8 +4373,8 @@ private module Subpaths {
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate hasSuccessor(PathNode pred, PathNodeMid succ, NodeEx succNode) {
|
||||
succ = pred.getASuccessor() and
|
||||
private predicate hasSuccessor(PathNodeImpl pred, PathNodeMid succ, NodeEx succNode) {
|
||||
succ = pred.getANonHiddenSuccessor() and
|
||||
succNode = succ.getNodeEx()
|
||||
}
|
||||
|
||||
@@ -4375,9 +4383,9 @@ private module Subpaths {
|
||||
* a subpath between `par` and `ret` with the connecting edges `arg -> par` and
|
||||
* `ret -> out` is summarized as the edge `arg -> out`.
|
||||
*/
|
||||
predicate subpaths(PathNode arg, PathNodeImpl par, PathNodeImpl ret, PathNode out) {
|
||||
predicate subpaths(PathNodeImpl arg, PathNodeImpl par, PathNodeImpl ret, PathNode out) {
|
||||
exists(ParamNodeEx p, NodeEx o, FlowState sout, AccessPath apout, PathNodeMid out0 |
|
||||
pragma[only_bind_into](arg).getASuccessor() = pragma[only_bind_into](out0) and
|
||||
pragma[only_bind_into](arg).getANonHiddenSuccessor() = pragma[only_bind_into](out0) and
|
||||
subpaths03(pragma[only_bind_into](arg), p, localStepToHidden*(ret), o, sout, apout) and
|
||||
hasSuccessor(pragma[only_bind_into](arg), par, p) and
|
||||
not ret.isHidden() and
|
||||
@@ -4390,12 +4398,12 @@ private module Subpaths {
|
||||
/**
|
||||
* Holds if `n` can reach a return node in a summarized subpath that can reach a sink.
|
||||
*/
|
||||
predicate retReach(PathNode n) {
|
||||
predicate retReach(PathNodeImpl n) {
|
||||
exists(PathNode out | subpaths(_, _, n, out) | directReach(out) or retReach(out))
|
||||
or
|
||||
exists(PathNode mid |
|
||||
exists(PathNodeImpl mid |
|
||||
retReach(mid) and
|
||||
n.getASuccessor() = mid and
|
||||
n.getANonHiddenSuccessor() = mid and
|
||||
not subpaths(_, mid, _, _)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3854,16 +3854,11 @@ class PathNode extends TPathNode {
|
||||
/** Gets the associated configuration. */
|
||||
Configuration getConfiguration() { none() }
|
||||
|
||||
private PathNode getASuccessorIfHidden() {
|
||||
this.(PathNodeImpl).isHidden() and
|
||||
result = this.(PathNodeImpl).getASuccessorImpl()
|
||||
}
|
||||
|
||||
/** Gets a successor of this node, if any. */
|
||||
final PathNode getASuccessor() {
|
||||
result = this.(PathNodeImpl).getASuccessorImpl().getASuccessorIfHidden*() and
|
||||
not this.(PathNodeImpl).isHidden() and
|
||||
not result.(PathNodeImpl).isHidden()
|
||||
result = this.(PathNodeImpl).getANonHiddenSuccessor() and
|
||||
reach(this) and
|
||||
reach(result)
|
||||
}
|
||||
|
||||
/** Holds if this node is a source. */
|
||||
@@ -3871,7 +3866,18 @@ class PathNode extends TPathNode {
|
||||
}
|
||||
|
||||
abstract private class PathNodeImpl extends PathNode {
|
||||
abstract PathNode getASuccessorImpl();
|
||||
abstract PathNodeImpl getASuccessorImpl();
|
||||
|
||||
private PathNodeImpl getASuccessorIfHidden() {
|
||||
this.isHidden() and
|
||||
result = this.getASuccessorImpl()
|
||||
}
|
||||
|
||||
final PathNodeImpl getANonHiddenSuccessor() {
|
||||
result = this.getASuccessorImpl().getASuccessorIfHidden*() and
|
||||
not this.isHidden() and
|
||||
not result.isHidden()
|
||||
}
|
||||
|
||||
abstract NodeEx getNodeEx();
|
||||
|
||||
@@ -3914,15 +3920,17 @@ abstract private class PathNodeImpl extends PathNode {
|
||||
}
|
||||
|
||||
/** Holds if `n` can reach a sink. */
|
||||
private predicate directReach(PathNode n) {
|
||||
n instanceof PathNodeSink or directReach(n.getASuccessor())
|
||||
private predicate directReach(PathNodeImpl n) {
|
||||
n instanceof PathNodeSink or directReach(n.getANonHiddenSuccessor())
|
||||
}
|
||||
|
||||
/** Holds if `n` can reach a sink or is used in a subpath that can reach a sink. */
|
||||
private predicate reach(PathNode n) { directReach(n) or Subpaths::retReach(n) }
|
||||
|
||||
/** Holds if `n1.getASuccessor() = n2` and `n2` can reach a sink. */
|
||||
private predicate pathSucc(PathNode n1, PathNode n2) { n1.getASuccessor() = n2 and directReach(n2) }
|
||||
private predicate pathSucc(PathNodeImpl n1, PathNode n2) {
|
||||
n1.getANonHiddenSuccessor() = n2 and directReach(n2)
|
||||
}
|
||||
|
||||
private predicate pathSuccPlus(PathNode n1, PathNode n2) = fastTC(pathSucc/2)(n1, n2)
|
||||
|
||||
@@ -3931,7 +3939,7 @@ private predicate pathSuccPlus(PathNode n1, PathNode n2) = fastTC(pathSucc/2)(n1
|
||||
*/
|
||||
module PathGraph {
|
||||
/** Holds if `(a,b)` is an edge in the graph of data flow path explanations. */
|
||||
query predicate edges(PathNode a, PathNode b) { a.getASuccessor() = b and reach(a) and reach(b) }
|
||||
query predicate edges(PathNode a, PathNode b) { a.getASuccessor() = b }
|
||||
|
||||
/** Holds if `n` is a node in the graph of data flow path explanations. */
|
||||
query predicate nodes(PathNode n, string key, string val) {
|
||||
@@ -4049,7 +4057,7 @@ private class PathNodeSink extends PathNodeImpl, TPathNodeSink {
|
||||
|
||||
override Configuration getConfiguration() { result = config }
|
||||
|
||||
override PathNode getASuccessorImpl() { none() }
|
||||
override PathNodeImpl getASuccessorImpl() { none() }
|
||||
|
||||
override predicate isSource() { sourceNode(node, state, config) }
|
||||
}
|
||||
@@ -4365,8 +4373,8 @@ private module Subpaths {
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate hasSuccessor(PathNode pred, PathNodeMid succ, NodeEx succNode) {
|
||||
succ = pred.getASuccessor() and
|
||||
private predicate hasSuccessor(PathNodeImpl pred, PathNodeMid succ, NodeEx succNode) {
|
||||
succ = pred.getANonHiddenSuccessor() and
|
||||
succNode = succ.getNodeEx()
|
||||
}
|
||||
|
||||
@@ -4375,9 +4383,9 @@ private module Subpaths {
|
||||
* a subpath between `par` and `ret` with the connecting edges `arg -> par` and
|
||||
* `ret -> out` is summarized as the edge `arg -> out`.
|
||||
*/
|
||||
predicate subpaths(PathNode arg, PathNodeImpl par, PathNodeImpl ret, PathNode out) {
|
||||
predicate subpaths(PathNodeImpl arg, PathNodeImpl par, PathNodeImpl ret, PathNode out) {
|
||||
exists(ParamNodeEx p, NodeEx o, FlowState sout, AccessPath apout, PathNodeMid out0 |
|
||||
pragma[only_bind_into](arg).getASuccessor() = pragma[only_bind_into](out0) and
|
||||
pragma[only_bind_into](arg).getANonHiddenSuccessor() = pragma[only_bind_into](out0) and
|
||||
subpaths03(pragma[only_bind_into](arg), p, localStepToHidden*(ret), o, sout, apout) and
|
||||
hasSuccessor(pragma[only_bind_into](arg), par, p) and
|
||||
not ret.isHidden() and
|
||||
@@ -4390,12 +4398,12 @@ private module Subpaths {
|
||||
/**
|
||||
* Holds if `n` can reach a return node in a summarized subpath that can reach a sink.
|
||||
*/
|
||||
predicate retReach(PathNode n) {
|
||||
predicate retReach(PathNodeImpl n) {
|
||||
exists(PathNode out | subpaths(_, _, n, out) | directReach(out) or retReach(out))
|
||||
or
|
||||
exists(PathNode mid |
|
||||
exists(PathNodeImpl mid |
|
||||
retReach(mid) and
|
||||
n.getASuccessor() = mid and
|
||||
n.getANonHiddenSuccessor() = mid and
|
||||
not subpaths(_, mid, _, _)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import python
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
abstract class TestScope extends Scope { }
|
||||
|
||||
// don't extend Class directly to avoid ambiguous method warnings
|
||||
class UnitTestClass extends TestScope {
|
||||
class UnitTestClass extends TestScope, Class {
|
||||
UnitTestClass() {
|
||||
exists(ClassValue cls | this = cls.getScope() |
|
||||
cls.getABaseType+() = Module::named("unittest").attr(_)
|
||||
or
|
||||
cls.getABaseType+().getName().toLowerCase() = "testcase"
|
||||
exists(API::Node testCaseClass, string testCaseString |
|
||||
testCaseString.matches("%TestCase") and
|
||||
testCaseClass = any(API::Node mod).getMember(testCaseString)
|
||||
|
|
||||
this.getParent() = testCaseClass.getASubclass*().getAnImmediateUse().asExpr()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,7 +662,7 @@ private module AiohttpClientModel {
|
||||
private API::Node instance() { result = classRef().getReturn() }
|
||||
|
||||
/** A method call on a ClientSession that sends off a request */
|
||||
private class OutgoingRequestCall extends HTTP::Client::Request::Range, DataFlow::CallCfgNode {
|
||||
private class OutgoingRequestCall extends HTTP::Client::Request::Range, API::CallNode {
|
||||
string methodName;
|
||||
|
||||
OutgoingRequestCall() {
|
||||
@@ -685,8 +685,14 @@ private module AiohttpClientModel {
|
||||
override predicate disablesCertificateValidation(
|
||||
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
|
||||
) {
|
||||
// TODO: Look into disabling certificate validation
|
||||
none()
|
||||
exists(API::Node param | param = this.getKeywordParameter(["ssl", "verify_ssl"]) |
|
||||
disablingNode = param.getARhs() and
|
||||
argumentOrigin = param.getAValueReachingRhs() and
|
||||
// aiohttp.client treats `None` as the default and all other "falsey" values as `False`.
|
||||
argumentOrigin.asExpr().(ImmutableLiteral).booleanValue() = false and
|
||||
not argumentOrigin.asExpr() instanceof None
|
||||
)
|
||||
// TODO: Handling of SSLContext passed as ssl/ssl_context arguments
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,91 +7,42 @@ private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
/** Provides models for the `asyncpg` PyPI package. */
|
||||
private module Asyncpg {
|
||||
private import semmle.python.internal.Awaited
|
||||
|
||||
/** Gets a `ConnectionPool` that is created when the result of `asyncpg.create_pool()` is awaited. */
|
||||
API::Node connectionPool() {
|
||||
result = API::moduleImport("asyncpg").getMember("create_pool").getReturn().getAwaited()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a `Connection` that is created when
|
||||
* - the result of `asyncpg.connect()` is awaited.
|
||||
* - the result of calling `acquire` 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() {
|
||||
this = [connectionPool(), connection()].getMember(methodName).getACall() and
|
||||
methodName in ["copy_from_query", "execute", "fetch", "fetchrow", "fetchval", "executemany"]
|
||||
}
|
||||
|
||||
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")]
|
||||
class AsyncpgModel extends ModelInput::TypeModelCsv {
|
||||
override predicate row(string row) {
|
||||
// package1;type1;package2;type2;path
|
||||
row =
|
||||
[
|
||||
// a `ConnectionPool` that is created when the result of `asyncpg.create_pool()` is awaited.
|
||||
"asyncpg;ConnectionPool;asyncpg;;Member[create_pool].ReturnValue.Awaited",
|
||||
// a `Connection` that is created when
|
||||
// * - the result of `asyncpg.connect()` is awaited.
|
||||
// * - the result of calling `acquire` on a `ConnectionPool` is awaited.
|
||||
"asyncpg;Connection;asyncpg;;Member[connect].ReturnValue.Awaited",
|
||||
"asyncpg;Connection;asyncpg;ConnectionPool;Member[acquire].ReturnValue.Awaited",
|
||||
// Creating an internal `~Connection` type that contains both `Connection` and `ConnectionPool`.
|
||||
"asyncpg;~Connection;asyncpg;Connection;", "asyncpg;~Connection;asyncpg;ConnectionPool;"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/** A model of `Connection` and `ConnectionPool`, which provide some methods that access the file system. */
|
||||
class FileAccessOnConnection extends FileSystemAccess::Range, DataFlow::MethodCallNode {
|
||||
string methodName;
|
||||
|
||||
FileAccessOnConnection() {
|
||||
this = [connectionPool(), connection()].getMember(methodName).getACall() and
|
||||
methodName in ["copy_from_query", "copy_from_table", "copy_to_table"]
|
||||
}
|
||||
|
||||
// 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 further to any executing methods.
|
||||
* Hence the two type trackers.
|
||||
*/
|
||||
module PreparedStatement {
|
||||
class PreparedStatementConstruction extends SqlConstruction::Range, API::CallNode {
|
||||
PreparedStatementConstruction() { this = connection().getMember("prepare").getACall() }
|
||||
|
||||
override DataFlow::Node getSql() { result = this.getParameter(0, "query").getARhs() }
|
||||
}
|
||||
|
||||
class PreparedStatementExecution extends SqlExecution::Range, API::CallNode {
|
||||
PreparedStatementConstruction prepareCall;
|
||||
|
||||
PreparedStatementExecution() {
|
||||
this =
|
||||
prepareCall
|
||||
.getReturn()
|
||||
.getAwaited()
|
||||
.getMember(["executemany", "fetch", "fetchrow", "fetchval"])
|
||||
.getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getSql() { result = prepareCall.getSql() }
|
||||
class AsyncpgSink extends ModelInput::SinkModelCsv {
|
||||
// package;type;path;kind
|
||||
override predicate row(string row) {
|
||||
row =
|
||||
[
|
||||
// `Connection`s and `ConnectionPool`s provide some methods that execute SQL.
|
||||
"asyncpg;~Connection;Member[copy_from_query,execute,fetch,fetchrow,fetchval].Argument[0,query:];sql-injection",
|
||||
"asyncpg;~Connection;Member[executemany].Argument[0,command:];sql-injection",
|
||||
// A model of `Connection` and `ConnectionPool`, which provide some methods that access the file system.
|
||||
"asyncpg;~Connection;Member[copy_from_query,copy_from_table].Argument[output:];path-injection",
|
||||
"asyncpg;~Connection;Member[copy_to_table].Argument[source:];path-injection",
|
||||
// the `PreparedStatement` class in `asyncpg`.
|
||||
"asyncpg;Connection;Member[prepare].Argument[0,query:];sql-injection",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +57,9 @@ private module Asyncpg {
|
||||
*/
|
||||
module Cursor {
|
||||
class CursorConstruction extends SqlConstruction::Range, API::CallNode {
|
||||
CursorConstruction() { this = connection().getMember("cursor").getACall() }
|
||||
CursorConstruction() {
|
||||
this = ModelOutput::getATypeNode("asyncpg", "Connection").getMember("cursor").getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getSql() { result = this.getParameter(0, "query").getARhs() }
|
||||
}
|
||||
@@ -121,8 +74,11 @@ private module Asyncpg {
|
||||
this = c.getReturn().getAwaited().getAnImmediateUse()
|
||||
)
|
||||
or
|
||||
exists(PreparedStatement::PreparedStatementConstruction prepareCall |
|
||||
sql = prepareCall.getSql() and
|
||||
exists(API::CallNode prepareCall |
|
||||
prepareCall =
|
||||
ModelOutput::getATypeNode("asyncpg", "Connection").getMember("prepare").getACall()
|
||||
|
|
||||
sql = prepareCall.getParameter(0, "query").getARhs() and
|
||||
this =
|
||||
prepareCall
|
||||
.getReturn()
|
||||
|
||||
@@ -108,20 +108,20 @@ private module CryptodomeModel {
|
||||
DataFlow::CallCfgNode {
|
||||
string methodName;
|
||||
string cipherName;
|
||||
API::CallNode newCall;
|
||||
|
||||
CryptodomeGenericCipherOperation() {
|
||||
methodName in [
|
||||
"encrypt", "decrypt", "verify", "update", "hexverify", "encrypt_and_digest",
|
||||
"decrypt_and_verify"
|
||||
] and
|
||||
this =
|
||||
newCall =
|
||||
API::moduleImport(["Crypto", "Cryptodome"])
|
||||
.getMember("Cipher")
|
||||
.getMember(cipherName)
|
||||
.getMember("new")
|
||||
.getReturn()
|
||||
.getMember(methodName)
|
||||
.getACall()
|
||||
.getACall() and
|
||||
this = newCall.getReturn().getMember(methodName).getACall()
|
||||
}
|
||||
|
||||
override Cryptography::CryptographicAlgorithm getAlgorithm() { result.matchesName(cipherName) }
|
||||
@@ -155,6 +155,20 @@ private module CryptodomeModel {
|
||||
this.getArgByName("mac_tag")
|
||||
]
|
||||
}
|
||||
|
||||
override Cryptography::BlockMode getBlockMode() {
|
||||
// `modeName` is of the form "MODE_<BlockMode>"
|
||||
exists(string modeName |
|
||||
newCall.getArg(1) =
|
||||
API::moduleImport(["Crypto", "Cryptodome"])
|
||||
.getMember("Cipher")
|
||||
.getMember(cipherName)
|
||||
.getMember(modeName)
|
||||
.getAUse()
|
||||
|
|
||||
result = modeName.splitAt("_", 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,6 +206,8 @@ private module CryptodomeModel {
|
||||
result in [this.getArg(1), this.getArgByName("signature")]
|
||||
)
|
||||
}
|
||||
|
||||
override Cryptography::BlockMode getBlockMode() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,5 +231,7 @@ private module CryptodomeModel {
|
||||
override Cryptography::CryptographicAlgorithm getAlgorithm() { result.matchesName(hashName) }
|
||||
|
||||
override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("data")] }
|
||||
|
||||
override Cryptography::BlockMode getBlockMode() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,8 +170,19 @@ private module CryptographyModel {
|
||||
.getMember(algorithmName)
|
||||
}
|
||||
|
||||
/** Gets a reference to a `cryptography.hazmat.primitives.ciphers.modes` Class */
|
||||
API::Node modeClassRef(string modeName) {
|
||||
result =
|
||||
API::moduleImport("cryptography")
|
||||
.getMember("hazmat")
|
||||
.getMember("primitives")
|
||||
.getMember("ciphers")
|
||||
.getMember("modes")
|
||||
.getMember(modeName)
|
||||
}
|
||||
|
||||
/** Gets a reference to a Cipher instance using algorithm with `algorithmName`. */
|
||||
API::Node cipherInstance(string algorithmName) {
|
||||
API::Node cipherInstance(string algorithmName, string modeName) {
|
||||
exists(API::CallNode call | result = call.getReturn() |
|
||||
call =
|
||||
API::moduleImport("cryptography")
|
||||
@@ -182,7 +193,12 @@ private module CryptographyModel {
|
||||
.getACall() and
|
||||
algorithmClassRef(algorithmName).getReturn().getAUse() in [
|
||||
call.getArg(0), call.getArgByName("algorithm")
|
||||
]
|
||||
] and
|
||||
exists(DataFlow::Node modeArg | modeArg in [call.getArg(1), call.getArgByName("mode")] |
|
||||
if modeArg = modeClassRef(_).getReturn().getAUse()
|
||||
then modeArg = modeClassRef(modeName).getReturn().getAUse()
|
||||
else modeName = "<None or unknown>"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -192,10 +208,11 @@ private module CryptographyModel {
|
||||
class CryptographyGenericCipherOperation extends Cryptography::CryptographicOperation::Range,
|
||||
DataFlow::MethodCallNode {
|
||||
string algorithmName;
|
||||
string modeName;
|
||||
|
||||
CryptographyGenericCipherOperation() {
|
||||
this =
|
||||
cipherInstance(algorithmName)
|
||||
cipherInstance(algorithmName, modeName)
|
||||
.getMember(["decryptor", "encryptor"])
|
||||
.getReturn()
|
||||
.getMember(["update", "update_into"])
|
||||
@@ -207,6 +224,8 @@ private module CryptographyModel {
|
||||
}
|
||||
|
||||
override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("data")] }
|
||||
|
||||
override Cryptography::BlockMode getBlockMode() { result = modeName }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +276,8 @@ private module CryptographyModel {
|
||||
}
|
||||
|
||||
override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("data")] }
|
||||
|
||||
override Cryptography::BlockMode getBlockMode() { none() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,7 +464,15 @@ module Django {
|
||||
|
||||
/** A file-like object instance that originates from a `UploadedFile`. */
|
||||
class UploadedFileFileLikeInstances extends Stdlib::FileLikeObject::InstanceSource {
|
||||
UploadedFileFileLikeInstances() { this.(DataFlow::AttrRead).accesses(instance(), "file") }
|
||||
UploadedFileFileLikeInstances() {
|
||||
// in the bottom of
|
||||
// https://docs.djangoproject.com/en/4.0/ref/files/file/#django.core.files.File
|
||||
// it's mentioned that the File object itself has proxy methods for
|
||||
// `read`/`write`/... that forwards to the underlying file object.
|
||||
this = instance()
|
||||
or
|
||||
this.(DataFlow::AttrRead).accesses(instance(), "file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,12 @@ private import semmle.python.ApiGraphs
|
||||
* - https://www.python-httpx.org/
|
||||
*/
|
||||
private module HttpxModel {
|
||||
private class RequestCall extends HTTP::Client::Request::Range, DataFlow::CallCfgNode {
|
||||
/**
|
||||
* An outgoing HTTP request, from the `httpx` library.
|
||||
*
|
||||
* See https://www.python-httpx.org/api/
|
||||
*/
|
||||
private class RequestCall extends HTTP::Client::Request::Range, API::CallNode {
|
||||
string methodName;
|
||||
|
||||
RequestCall() {
|
||||
@@ -39,15 +44,18 @@ private module HttpxModel {
|
||||
override predicate disablesCertificateValidation(
|
||||
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
|
||||
) {
|
||||
// TODO: Look into disabling certificate validation
|
||||
none()
|
||||
disablingNode = this.getKeywordParameter("verify").getARhs() and
|
||||
argumentOrigin = this.getKeywordParameter("verify").getAValueReachingRhs() and
|
||||
// unlike `requests`, httpx treats `None` as turning off verify (and not as the default)
|
||||
argumentOrigin.asExpr().(ImmutableLiteral).booleanValue() = false
|
||||
// TODO: Handling of insecure SSLContext passed to verify argument
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `httpx.[Async]Client` class
|
||||
*
|
||||
* See https://www.python-httpx.org/async/
|
||||
* See https://www.python-httpx.org/api/#client
|
||||
*/
|
||||
module Client {
|
||||
/** Get a reference to the `httpx.Client` or `httpx.AsyncClient` class. */
|
||||
@@ -55,16 +63,13 @@ private module HttpxModel {
|
||||
result = API::moduleImport("httpx").getMember(["Client", "AsyncClient"])
|
||||
}
|
||||
|
||||
/** Get a reference to an `httpx.Client` or `httpx.AsyncClient` instance. */
|
||||
private API::Node instance() { result = classRef().getReturn() }
|
||||
|
||||
/** A method call on a Client that sends off a request */
|
||||
private class OutgoingRequestCall extends HTTP::Client::Request::Range, DataFlow::CallCfgNode {
|
||||
string methodName;
|
||||
|
||||
OutgoingRequestCall() {
|
||||
methodName in [HTTP::httpVerbLower(), "request", "stream"] and
|
||||
this = instance().getMember(methodName).getACall()
|
||||
this = classRef().getReturn().getMember(methodName).getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAUrlPart() {
|
||||
@@ -80,8 +85,16 @@ private module HttpxModel {
|
||||
override predicate disablesCertificateValidation(
|
||||
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
|
||||
) {
|
||||
// TODO: Look into disabling certificate validation
|
||||
none()
|
||||
exists(API::CallNode constructor |
|
||||
constructor = classRef().getACall() and
|
||||
this = constructor.getReturn().getMember(methodName).getACall()
|
||||
|
|
||||
disablingNode = constructor.getKeywordParameter("verify").getARhs() and
|
||||
argumentOrigin = constructor.getKeywordParameter("verify").getAValueReachingRhs() and
|
||||
// unlike `requests`, httpx treats `None` as turning off verify (and not as the default)
|
||||
argumentOrigin.asExpr().(ImmutableLiteral).booleanValue() = false
|
||||
// TODO: Handling of insecure SSLContext passed to verify argument
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,14 @@ private import semmle.python.frameworks.Stdlib
|
||||
*
|
||||
* See
|
||||
* - https://pypi.org/project/requests/
|
||||
* - https://docs.python-requests.org/en/latest/
|
||||
* - https://requests.readthedocs.io/en/latest/
|
||||
*/
|
||||
private module Requests {
|
||||
/**
|
||||
* An outgoing HTTP request, from the `requests` library.
|
||||
*
|
||||
* See https://requests.readthedocs.io/en/latest/api/#requests.request
|
||||
*/
|
||||
private class OutgoingRequestCall extends HTTP::Client::Request::Range, API::CallNode {
|
||||
string methodName;
|
||||
|
||||
@@ -58,6 +63,7 @@ private module Requests {
|
||||
) {
|
||||
disablingNode = this.getKeywordParameter("verify").getARhs() and
|
||||
argumentOrigin = this.getKeywordParameter("verify").getAValueReachingRhs() and
|
||||
// requests treats `None` as the default and all other "falsey" values as `False`.
|
||||
argumentOrigin.asExpr().(ImmutableLiteral).booleanValue() = false and
|
||||
not argumentOrigin.asExpr() instanceof None
|
||||
}
|
||||
@@ -81,7 +87,7 @@ private module Requests {
|
||||
/**
|
||||
* Provides models for the `requests.models.Response` class
|
||||
*
|
||||
* See https://docs.python-requests.org/en/latest/api/#requests.Response.
|
||||
* See https://requests.readthedocs.io/en/latest/api/#requests.Response.
|
||||
*/
|
||||
module Response {
|
||||
/** Gets a reference to the `requests.models.Response` class. */
|
||||
|
||||
@@ -41,6 +41,8 @@ private module Rsa {
|
||||
override DataFlow::Node getAnInput() {
|
||||
result in [this.getArg(0), this.getArgByName("message")]
|
||||
}
|
||||
|
||||
override Cryptography::BlockMode getBlockMode() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,6 +56,8 @@ private module Rsa {
|
||||
override Cryptography::CryptographicAlgorithm getAlgorithm() { result.getName() = "RSA" }
|
||||
|
||||
override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("crypto")] }
|
||||
|
||||
override Cryptography::BlockMode getBlockMode() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,6 +83,8 @@ private module Rsa {
|
||||
override DataFlow::Node getAnInput() {
|
||||
result in [this.getArg(0), this.getArgByName("message")]
|
||||
}
|
||||
|
||||
override Cryptography::BlockMode getBlockMode() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,6 +106,8 @@ private module Rsa {
|
||||
or
|
||||
result in [this.getArg(1), this.getArgByName("signature")]
|
||||
}
|
||||
|
||||
override Cryptography::BlockMode getBlockMode() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,6 +130,8 @@ private module Rsa {
|
||||
override DataFlow::Node getAnInput() {
|
||||
result in [this.getArg(0), this.getArgByName("message")]
|
||||
}
|
||||
|
||||
override Cryptography::BlockMode getBlockMode() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,5 +147,7 @@ private module Rsa {
|
||||
override DataFlow::Node getAnInput() {
|
||||
result in [this.getArg(0), this.getArgByName("hash_value")]
|
||||
}
|
||||
|
||||
override Cryptography::BlockMode getBlockMode() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2671,6 +2671,8 @@ private module StdlibPrivate {
|
||||
override Cryptography::CryptographicAlgorithm getAlgorithm() { result.matchesName(hashName) }
|
||||
|
||||
override DataFlow::Node getAnInput() { result = this.getParameter(1, "data").getARhs() }
|
||||
|
||||
override Cryptography::BlockMode getBlockMode() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2686,6 +2688,8 @@ private module StdlibPrivate {
|
||||
override Cryptography::CryptographicAlgorithm getAlgorithm() { result.matchesName(hashName) }
|
||||
|
||||
override DataFlow::Node getAnInput() { result = this.getArg(0) }
|
||||
|
||||
override Cryptography::BlockMode getBlockMode() { none() }
|
||||
}
|
||||
|
||||
/** Helper predicate for the `HashLibGenericHashOperation` charpred, to prevent a bad join order. */
|
||||
@@ -2709,6 +2713,8 @@ private module StdlibPrivate {
|
||||
HashlibGenericHashOperation() { hashClass = hashlibMember(hashName) }
|
||||
|
||||
override Cryptography::CryptographicAlgorithm getAlgorithm() { result.matchesName(hashName) }
|
||||
|
||||
override Cryptography::BlockMode getBlockMode() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,7 +42,8 @@ private module Urllib {
|
||||
override predicate disablesCertificateValidation(
|
||||
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
|
||||
) {
|
||||
// TODO: Look into disabling certificate validation
|
||||
// cannot enable/disable certificate validation on this object, only when used
|
||||
// with `urlopen`, which is modeled below
|
||||
none()
|
||||
}
|
||||
}
|
||||
@@ -63,7 +64,8 @@ private module Urllib {
|
||||
override predicate disablesCertificateValidation(
|
||||
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
|
||||
) {
|
||||
// TODO: Look into disabling certificate validation
|
||||
// will validate certificate by default, see https://github.com/python/cpython/blob/243ed5439c32e8517aa745bc2ca9774d99c99d0f/Lib/http/client.py#L1420-L1421
|
||||
// TODO: Handling of insecure SSLContext passed to context argument
|
||||
none()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ private module Urllib2 {
|
||||
override predicate disablesCertificateValidation(
|
||||
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
|
||||
) {
|
||||
// TODO: Look into disabling certificate validation
|
||||
// cannot enable/disable certificate validation on this object, only when used
|
||||
// with `urlopen`, which is modeled below
|
||||
none()
|
||||
}
|
||||
}
|
||||
@@ -49,7 +50,8 @@ private module Urllib2 {
|
||||
override predicate disablesCertificateValidation(
|
||||
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
|
||||
) {
|
||||
// TODO: Look into disabling certificate validation
|
||||
// will validate certificate by default
|
||||
// TODO: Handling of insecure SSLContext passed to context argument
|
||||
none()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,6 @@ private module Urllib3 {
|
||||
.getASubclass+()
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of a `urllib3.request.RequestMethods` subclass. */
|
||||
private API::Node instance() { result = classRef().getReturn() }
|
||||
|
||||
/**
|
||||
* A call to a method making an outgoing request.
|
||||
*
|
||||
@@ -52,10 +49,11 @@ private module Urllib3 {
|
||||
* - https://urllib3.readthedocs.io/en/stable/reference/urllib3.request.html#urllib3.request.RequestMethods
|
||||
* - https://urllib3.readthedocs.io/en/stable/reference/urllib3.connectionpool.html#urllib3.HTTPConnectionPool.urlopen
|
||||
*/
|
||||
private class RequestCall extends HTTP::Client::Request::Range, DataFlow::CallCfgNode {
|
||||
private class RequestCall extends HTTP::Client::Request::Range, API::CallNode {
|
||||
RequestCall() {
|
||||
this =
|
||||
instance()
|
||||
classRef()
|
||||
.getReturn()
|
||||
.getMember(["request", "request_encode_url", "request_encode_body", "urlopen"])
|
||||
.getACall()
|
||||
}
|
||||
@@ -67,8 +65,22 @@ private module Urllib3 {
|
||||
override predicate disablesCertificateValidation(
|
||||
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
|
||||
) {
|
||||
// TODO: Look into disabling certificate validation
|
||||
none()
|
||||
exists(API::CallNode constructor |
|
||||
constructor = classRef().getACall() and
|
||||
this = constructor.getReturn().getAMember().getACall()
|
||||
|
|
||||
// cert_reqs
|
||||
// see https://urllib3.readthedocs.io/en/stable/user-guide.html?highlight=cert_reqs#certificate-verification
|
||||
disablingNode = constructor.getKeywordParameter("cert_reqs").getARhs() and
|
||||
argumentOrigin = constructor.getKeywordParameter("cert_reqs").getAValueReachingRhs() and
|
||||
argumentOrigin.asExpr().(StrConst).getText() = "CERT_NONE"
|
||||
or
|
||||
// assert_hostname
|
||||
// see https://urllib3.readthedocs.io/en/stable/reference/urllib3.connectionpool.html?highlight=assert_hostname#urllib3.HTTPSConnectionPool
|
||||
disablingNode = constructor.getKeywordParameter("assert_hostname").getARhs() and
|
||||
argumentOrigin = constructor.getKeywordParameter("assert_hostname").getAValueReachingRhs() and
|
||||
argumentOrigin.asExpr().(BooleanLiteral).booleanValue() = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
47
python/ql/lib/semmle/python/frameworks/data/ModelsAsData.qll
Normal file
47
python/ql/lib/semmle/python/frameworks/data/ModelsAsData.qll
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Provides classes for contributing a model, or using the interpreted results
|
||||
* of a model represented as data.
|
||||
*
|
||||
* - Use the `ModelInput` module to contribute new models.
|
||||
* - Use the `ModelOutput` module to access the model results in terms of API nodes.
|
||||
*
|
||||
* The package name refers to the top-level module the import comes from, and not a PyPI package.
|
||||
* So for `from foo.bar import baz`, the package will be `foo`.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import internal.ApiGraphModels as Shared
|
||||
private import internal.ApiGraphModelsSpecific as Specific
|
||||
import Shared::ModelInput as ModelInput
|
||||
import Shared::ModelOutput as ModelOutput
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
|
||||
/**
|
||||
* A remote flow source originating from a CSV source row.
|
||||
*/
|
||||
private class RemoteFlowSourceFromCsv extends RemoteFlowSource {
|
||||
RemoteFlowSourceFromCsv() { this = ModelOutput::getASourceNode("remote").getAnImmediateUse() }
|
||||
|
||||
override string getSourceType() { result = "Remote flow (from model)" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `ModelOutput::summaryStep` but with API nodes mapped to data-flow nodes.
|
||||
*/
|
||||
private predicate summaryStepNodes(DataFlow::Node pred, DataFlow::Node succ, string kind) {
|
||||
exists(API::Node predNode, API::Node succNode |
|
||||
Specific::summaryStep(predNode, succNode, kind) and
|
||||
pred = predNode.getARhs() and
|
||||
succ = succNode.getAnImmediateUse()
|
||||
)
|
||||
}
|
||||
|
||||
/** Taint steps induced by summary models of kind `taint`. */
|
||||
private class TaintStepFromSummary extends TaintTracking::AdditionalTaintStep {
|
||||
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
summaryStepNodes(pred, succ, "taint")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Module for parsing access paths from CSV models, both the identifying access path used
|
||||
* by dynamic languages, and the input/output specifications for summary steps.
|
||||
*
|
||||
* This file is used by the shared data flow library and by the JavaScript libraries
|
||||
* (which does not use the shared data flow libraries).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convenience-predicate for extracting two capture groups at once.
|
||||
*/
|
||||
bindingset[input, regexp]
|
||||
private predicate regexpCaptureTwo(string input, string regexp, string capture1, string capture2) {
|
||||
capture1 = input.regexpCapture(regexp, 1) and
|
||||
capture2 = input.regexpCapture(regexp, 2)
|
||||
}
|
||||
|
||||
/** Companion module to the `AccessPath` class. */
|
||||
module AccessPath {
|
||||
/** A string that should be parsed as an access path. */
|
||||
abstract class Range extends string {
|
||||
bindingset[this]
|
||||
Range() { any() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an integer constant `n` or interval `n1..n2` (inclusive) and gets the value
|
||||
* of the constant or any value contained in the interval.
|
||||
*/
|
||||
bindingset[arg]
|
||||
int parseInt(string arg) {
|
||||
result = arg.toInt()
|
||||
or
|
||||
// Match "n1..n2"
|
||||
exists(string lo, string hi |
|
||||
regexpCaptureTwo(arg, "(-?\\d+)\\.\\.(-?\\d+)", lo, hi) and
|
||||
result = [lo.toInt() .. hi.toInt()]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a lower-bounded interval `n..` and gets the lower bound.
|
||||
*/
|
||||
bindingset[arg]
|
||||
int parseLowerBound(string arg) { result = arg.regexpCapture("(-?\\d+)\\.\\.", 1).toInt() }
|
||||
|
||||
/**
|
||||
* Parses an integer constant or interval (bounded or unbounded) that explicitly
|
||||
* references the arity, such as `N-1` or `N-3..N-1`.
|
||||
*
|
||||
* Note that expressions of form `N-x` will never resolve to a negative index,
|
||||
* even if `N` is zero (it will have no result in that case).
|
||||
*/
|
||||
bindingset[arg, arity]
|
||||
private int parseIntWithExplicitArity(string arg, int arity) {
|
||||
result >= 0 and // do not allow N-1 to resolve to a negative index
|
||||
exists(string lo |
|
||||
// N-x
|
||||
lo = arg.regexpCapture("N-(\\d+)", 1) and
|
||||
result = arity - lo.toInt()
|
||||
or
|
||||
// N-x..
|
||||
lo = arg.regexpCapture("N-(\\d+)\\.\\.", 1) and
|
||||
result = [arity - lo.toInt(), arity - 1]
|
||||
)
|
||||
or
|
||||
exists(string lo, string hi |
|
||||
// x..N-y
|
||||
regexpCaptureTwo(arg, "(-?\\d+)\\.\\.N-(\\d+)", lo, hi) and
|
||||
result = [lo.toInt() .. arity - hi.toInt()]
|
||||
or
|
||||
// N-x..N-y
|
||||
regexpCaptureTwo(arg, "N-(\\d+)\\.\\.N-(\\d+)", lo, hi) and
|
||||
result = [arity - lo.toInt() .. arity - hi.toInt()] and
|
||||
result >= 0
|
||||
or
|
||||
// N-x..y
|
||||
regexpCaptureTwo(arg, "N-(\\d+)\\.\\.(\\d+)", lo, hi) and
|
||||
result = [arity - lo.toInt() .. hi.toInt()] and
|
||||
result >= 0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an integer constant or interval (bounded or unbounded) and gets any
|
||||
* of the integers contained within (of which there may be infinitely many).
|
||||
*
|
||||
* Has no result for arguments involving an explicit arity, such as `N-1`.
|
||||
*/
|
||||
bindingset[arg, result]
|
||||
int parseIntUnbounded(string arg) {
|
||||
result = parseInt(arg)
|
||||
or
|
||||
result >= parseLowerBound(arg)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an integer constant or interval (bounded or unbounded) that
|
||||
* may reference the arity of a call, such as `N-1` or `N-3..N-1`.
|
||||
*
|
||||
* Note that expressions of form `N-x` will never resolve to a negative index,
|
||||
* even if `N` is zero (it will have no result in that case).
|
||||
*/
|
||||
bindingset[arg, arity]
|
||||
int parseIntWithArity(string arg, int arity) {
|
||||
result = parseInt(arg)
|
||||
or
|
||||
result in [parseLowerBound(arg) .. arity - 1]
|
||||
or
|
||||
result = parseIntWithExplicitArity(arg, arity)
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets the `n`th token on the access path as a string. */
|
||||
private string getRawToken(AccessPath path, int n) {
|
||||
// Avoid splitting by '.' since tokens may contain dots, e.g. `Field[foo.Bar.x]`.
|
||||
// Instead use regexpFind to match valid tokens, and supplement with a final length
|
||||
// check (in `AccessPath.hasSyntaxError`) to ensure all characters were included in a token.
|
||||
result = path.regexpFind("\\w+(?:\\[[^\\]]*\\])?(?=\\.|$)", n, _)
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that occurs as an access path (either identifying or input/output spec)
|
||||
* which might be relevant for this database.
|
||||
*/
|
||||
class AccessPath extends string instanceof AccessPath::Range {
|
||||
/** Holds if this string is not a syntactically valid access path. */
|
||||
predicate hasSyntaxError() {
|
||||
// If the lengths match, all characters must haven been included in a token
|
||||
// or seen by the `.` lookahead pattern.
|
||||
this != "" and
|
||||
not this.length() = sum(int n | | getRawToken(this, n).length() + 1) - 1
|
||||
}
|
||||
|
||||
/** Gets the `n`th token on the access path (if there are no syntax errors). */
|
||||
AccessPathToken getToken(int n) {
|
||||
result = getRawToken(this, n) and
|
||||
not this.hasSyntaxError()
|
||||
}
|
||||
|
||||
/** Gets the number of tokens on the path (if there are no syntax errors). */
|
||||
int getNumToken() {
|
||||
result = count(int n | exists(getRawToken(this, n))) and
|
||||
not this.hasSyntaxError()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An access part token such as `Argument[1]` or `ReturnValue`, appearing in one or more access paths.
|
||||
*/
|
||||
class AccessPathToken extends string {
|
||||
AccessPathToken() { this = getRawToken(_, _) }
|
||||
|
||||
private string getPart(int part) {
|
||||
result = this.regexpCapture("([^\\[]+)(?:\\[([^\\]]*)\\])?", part)
|
||||
}
|
||||
|
||||
/** Gets the name of the token, such as `Member` from `Member[x]` */
|
||||
string getName() { result = this.getPart(1) }
|
||||
|
||||
/**
|
||||
* Gets the argument list, such as `1,2` from `Member[1,2]`,
|
||||
* or has no result if there are no arguments.
|
||||
*/
|
||||
string getArgumentList() { result = this.getPart(2) }
|
||||
|
||||
/** Gets the `n`th argument to this token, such as `x` or `y` from `Member[x,y]`. */
|
||||
string getArgument(int n) { result = this.getArgumentList().splitAt(",", n).trim() }
|
||||
|
||||
/** Gets the `n`th argument to this `name` token, such as `x` or `y` from `Member[x,y]`. */
|
||||
pragma[nomagic]
|
||||
string getArgument(string name, int n) { name = this.getName() and result = this.getArgument(n) }
|
||||
|
||||
/** Gets an argument to this token, such as `x` or `y` from `Member[x,y]`. */
|
||||
string getAnArgument() { result = this.getArgument(_) }
|
||||
|
||||
/** Gets an argument to this `name` token, such as `x` or `y` from `Member[x,y]`. */
|
||||
string getAnArgument(string name) { result = this.getArgument(name, _) }
|
||||
|
||||
/** Gets the number of arguments to this token, such as 2 for `Member[x,y]` or zero for `ReturnValue`. */
|
||||
int getNumArgument() { result = count(int n | exists(this.getArgument(n))) }
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
/**
|
||||
* INTERNAL use only. This is an experimental API subject to change without notice.
|
||||
*
|
||||
* Provides classes and predicates for dealing with flow models specified in CSV format.
|
||||
*
|
||||
* The CSV specification has the following columns:
|
||||
* - Sources:
|
||||
* `package; type; path; kind`
|
||||
* - Sinks:
|
||||
* `package; type; path; kind`
|
||||
* - Summaries:
|
||||
* `package; type; path; input; output; kind`
|
||||
* - Types:
|
||||
* `package1; type1; package2; type2; path`
|
||||
*
|
||||
* The interpretation of a row is similar to API-graphs with a left-to-right
|
||||
* reading.
|
||||
* 1. The `package` column selects a package name, as it would be referenced in the source code,
|
||||
* such as an NPM package, PIP package, or Ruby gem. (See `ModelsAsData.qll` for language-specific details).
|
||||
* It may also be a synthetic package used for a type definition (see type definitions below).
|
||||
* 2. The `type` column selects all instances of a named type originating from that package,
|
||||
* or the empty string if referring to the package itself.
|
||||
* It can also be a synthetic type name defined by a type definition (see type definitions below).
|
||||
* 3. The `path` column is a `.`-separated list of "access path tokens" to resolve, starting at the node selected by `package` and `type`.
|
||||
*
|
||||
* Every language supports the following tokens:
|
||||
* - Argument[n]: the n-th argument to a call. May be a range of form `x..y` (inclusive) and/or a comma-separated list.
|
||||
* Additionally, `N-1` refers to the last argument, `N-2` refers to the second-last, and so on.
|
||||
* - Parameter[n]: the n-th parameter of a callback. May be a range of form `x..y` (inclusive) and/or a comma-separated list.
|
||||
* - ReturnValue: the value returned by a function call
|
||||
* - WithArity[n]: match a call with the given arity. May be a range of form `x..y` (inclusive) and/or a comma-separated list.
|
||||
*
|
||||
* The following tokens are common and should be implemented for languages where it makes sense:
|
||||
* - Member[x]: a member named `x`; exactly what a "member" is depends on the language. May be a comma-separated list of names.
|
||||
* - Instance: an instance of a class
|
||||
* - Subclass: a subclass of a class
|
||||
* - ArrayElement: an element of array
|
||||
* - Element: an element of a collection-like object
|
||||
* - MapKey: a key in map-like object
|
||||
* - MapValue: a value in a map-like object
|
||||
* - Awaited: the value from a resolved promise/future-like object
|
||||
*
|
||||
* For the time being, please consult `ApiGraphModelsSpecific.qll` to see which language-specific tokens are currently supported.
|
||||
*
|
||||
* 4. The `input` and `output` columns specify how data enters and leaves the element selected by the
|
||||
* first `(package, type, path)` tuple. Both strings are `.`-separated access paths
|
||||
* of the same syntax as the `path` column.
|
||||
* 5. The `kind` column is a tag that can be referenced from QL to determine to
|
||||
* which classes the interpreted elements should be added. For example, for
|
||||
* sources `"remote"` indicates a default remote flow source, and for summaries
|
||||
* `"taint"` indicates a default additional taint step and `"value"` indicates a
|
||||
* globally applicable value-preserving step.
|
||||
*
|
||||
* ### Types
|
||||
*
|
||||
* A type row of form `package1; type1; package2; type2; path` indicates that `package2; type2; path`
|
||||
* should be seen as an instance of the type `package1; type1`.
|
||||
*
|
||||
* A `(package,type)` pair may refer to a static type or a synthetic type name used internally in the model.
|
||||
* Synthetic type names can be used to reuse intermediate sub-paths, when there are multiple ways to access the same
|
||||
* element.
|
||||
* See `ModelsAsData.qll` for the language-specific interpretation of packages and static type names.
|
||||
*
|
||||
* By convention, if one wants to avoid clashes with static types from the package, the type name
|
||||
* should be prefixed with a tilde character (`~`). For example, `(foo, ~Bar)` can be used to indicate that
|
||||
* the type is related to the `foo` package but is not intended to match a static type.
|
||||
*/
|
||||
|
||||
private import ApiGraphModelsSpecific as Specific
|
||||
|
||||
private class Unit = Specific::Unit;
|
||||
|
||||
private module API = Specific::API;
|
||||
|
||||
private import Specific::AccessPathSyntax
|
||||
|
||||
/** Module containing hooks for providing input data to be interpreted as a model. */
|
||||
module ModelInput {
|
||||
/**
|
||||
* A unit class for adding additional source model rows.
|
||||
*
|
||||
* Extend this class to add additional source definitions.
|
||||
*/
|
||||
class SourceModelCsv extends Unit {
|
||||
/**
|
||||
* Holds if `row` specifies a source definition.
|
||||
*
|
||||
* A row of form
|
||||
* ```
|
||||
* package;type;path;kind
|
||||
* ```
|
||||
* indicates that the value at `(package, type, path)` should be seen as a flow
|
||||
* source of the given `kind`.
|
||||
*
|
||||
* The kind `remote` represents a general remote flow source.
|
||||
*/
|
||||
abstract predicate row(string row);
|
||||
}
|
||||
|
||||
/**
|
||||
* A unit class for adding additional sink model rows.
|
||||
*
|
||||
* Extend this class to add additional sink definitions.
|
||||
*/
|
||||
class SinkModelCsv extends Unit {
|
||||
/**
|
||||
* Holds if `row` specifies a sink definition.
|
||||
*
|
||||
* A row of form
|
||||
* ```
|
||||
* package;type;path;kind
|
||||
* ```
|
||||
* indicates that the value at `(package, type, path)` should be seen as a sink
|
||||
* of the given `kind`.
|
||||
*/
|
||||
abstract predicate row(string row);
|
||||
}
|
||||
|
||||
/**
|
||||
* A unit class for adding additional summary model rows.
|
||||
*
|
||||
* Extend this class to add additional flow summary definitions.
|
||||
*/
|
||||
class SummaryModelCsv extends Unit {
|
||||
/**
|
||||
* Holds if `row` specifies a summary definition.
|
||||
*
|
||||
* A row of form
|
||||
* ```
|
||||
* package;type;path;input;output;kind
|
||||
* ```
|
||||
* indicates that for each call to `(package, type, path)`, the value referred to by `input`
|
||||
* can flow to the value referred to by `output`.
|
||||
*
|
||||
* `kind` should be either `value` or `taint`, for value-preserving or taint-preserving steps,
|
||||
* respectively.
|
||||
*/
|
||||
abstract predicate row(string row);
|
||||
}
|
||||
|
||||
/**
|
||||
* A unit class for adding additional type model rows.
|
||||
*
|
||||
* Extend this class to add additional type definitions.
|
||||
*/
|
||||
class TypeModelCsv extends Unit {
|
||||
/**
|
||||
* Holds if `row` specifies a type definition.
|
||||
*
|
||||
* A row of form,
|
||||
* ```
|
||||
* package1;type1;package2;type2;path
|
||||
* ```
|
||||
* indicates that `(package2, type2, path)` should be seen as an instance of `(package1, type1)`.
|
||||
*/
|
||||
abstract predicate row(string row);
|
||||
}
|
||||
}
|
||||
|
||||
private import ModelInput
|
||||
|
||||
/**
|
||||
* An empty class, except in specific tests.
|
||||
*
|
||||
* If this is non-empty, all models are parsed even if the package is not
|
||||
* considered relevant for the current database.
|
||||
*/
|
||||
abstract class TestAllModels extends Unit { }
|
||||
|
||||
/**
|
||||
* Append `;dummy` to the value of `s` to work around the fact that `string.split(delim,n)`
|
||||
* does not preserve empty trailing substrings.
|
||||
*/
|
||||
bindingset[result]
|
||||
private string inversePad(string s) { s = result + ";dummy" }
|
||||
|
||||
private predicate sourceModel(string row) { any(SourceModelCsv s).row(inversePad(row)) }
|
||||
|
||||
private predicate sinkModel(string row) { any(SinkModelCsv s).row(inversePad(row)) }
|
||||
|
||||
private predicate summaryModel(string row) { any(SummaryModelCsv s).row(inversePad(row)) }
|
||||
|
||||
private predicate typeModel(string row) { any(TypeModelCsv s).row(inversePad(row)) }
|
||||
|
||||
/** Holds if a source model exists for the given parameters. */
|
||||
predicate sourceModel(string package, string type, string path, string kind) {
|
||||
exists(string row |
|
||||
sourceModel(row) and
|
||||
row.splitAt(";", 0) = package and
|
||||
row.splitAt(";", 1) = type and
|
||||
row.splitAt(";", 2) = path and
|
||||
row.splitAt(";", 3) = kind
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if a sink model exists for the given parameters. */
|
||||
private predicate sinkModel(string package, string type, string path, string kind) {
|
||||
exists(string row |
|
||||
sinkModel(row) and
|
||||
row.splitAt(";", 0) = package and
|
||||
row.splitAt(";", 1) = type and
|
||||
row.splitAt(";", 2) = path and
|
||||
row.splitAt(";", 3) = kind
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if a summary model `row` exists for the given parameters. */
|
||||
private predicate summaryModel(
|
||||
string package, string type, string path, string input, string output, string kind
|
||||
) {
|
||||
exists(string row |
|
||||
summaryModel(row) and
|
||||
row.splitAt(";", 0) = package and
|
||||
row.splitAt(";", 1) = type and
|
||||
row.splitAt(";", 2) = path and
|
||||
row.splitAt(";", 3) = input and
|
||||
row.splitAt(";", 4) = output and
|
||||
row.splitAt(";", 5) = kind
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if an type model exists for the given parameters. */
|
||||
private predicate typeModel(
|
||||
string package1, string type1, string package2, string type2, string path
|
||||
) {
|
||||
exists(string row |
|
||||
typeModel(row) and
|
||||
row.splitAt(";", 0) = package1 and
|
||||
row.splitAt(";", 1) = type1 and
|
||||
row.splitAt(";", 2) = package2 and
|
||||
row.splitAt(";", 3) = type2 and
|
||||
row.splitAt(";", 4) = path
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a package that should be seen as an alias for the given other `package`,
|
||||
* or the `package` itself.
|
||||
*/
|
||||
bindingset[package]
|
||||
bindingset[result]
|
||||
string getAPackageAlias(string package) {
|
||||
typeModel(package, "", result, "", "")
|
||||
or
|
||||
result = package
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if CSV rows involving `package` might be relevant for the analysis of this database.
|
||||
*/
|
||||
private predicate isRelevantPackage(string package) {
|
||||
(
|
||||
sourceModel(package, _, _, _) or
|
||||
sinkModel(package, _, _, _) or
|
||||
summaryModel(package, _, _, _, _, _) or
|
||||
typeModel(package, _, _, _, _)
|
||||
) and
|
||||
(
|
||||
Specific::isPackageUsed(package)
|
||||
or
|
||||
exists(TestAllModels t)
|
||||
)
|
||||
or
|
||||
exists(string other |
|
||||
isRelevantPackage(other) and
|
||||
typeModel(package, _, other, _, _)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `package,type,path` is used in some CSV row.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
predicate isRelevantFullPath(string package, string type, string path) {
|
||||
isRelevantPackage(package) and
|
||||
(
|
||||
sourceModel(package, type, path, _) or
|
||||
sinkModel(package, type, path, _) or
|
||||
summaryModel(package, type, path, _, _, _) or
|
||||
typeModel(_, _, package, type, path)
|
||||
)
|
||||
}
|
||||
|
||||
/** A string from a CSV row that should be parsed as an access path. */
|
||||
private class AccessPathRange extends AccessPath::Range {
|
||||
AccessPathRange() {
|
||||
isRelevantFullPath(_, _, this)
|
||||
or
|
||||
exists(string package | isRelevantPackage(package) |
|
||||
summaryModel(package, _, _, this, _, _) or
|
||||
summaryModel(package, _, _, _, this, _)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a successor of `node` in the API graph.
|
||||
*/
|
||||
bindingset[token]
|
||||
API::Node getSuccessorFromNode(API::Node node, AccessPathToken token) {
|
||||
// API graphs use the same label for arguments and parameters. An edge originating from a
|
||||
// use-node represents an argument, and an edge originating from a def-node represents a parameter.
|
||||
// We just map both to the same thing.
|
||||
token.getName() = ["Argument", "Parameter"] and
|
||||
result = node.getParameter(AccessPath::parseIntUnbounded(token.getAnArgument()))
|
||||
or
|
||||
token.getName() = "ReturnValue" and
|
||||
result = node.getReturn()
|
||||
or
|
||||
// Language-specific tokens
|
||||
result = Specific::getExtraSuccessorFromNode(node, token)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an API-graph successor for the given invocation.
|
||||
*/
|
||||
bindingset[token]
|
||||
API::Node getSuccessorFromInvoke(Specific::InvokeNode invoke, AccessPathToken token) {
|
||||
token.getName() = "Argument" and
|
||||
result =
|
||||
invoke
|
||||
.getParameter(AccessPath::parseIntWithArity(token.getAnArgument(), invoke.getNumArgument()))
|
||||
or
|
||||
token.getName() = "ReturnValue" and
|
||||
result = invoke.getReturn()
|
||||
or
|
||||
// Language-specific tokens
|
||||
result = Specific::getExtraSuccessorFromInvoke(invoke, token)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `invoke` invokes a call-site filter given by `token`.
|
||||
*/
|
||||
pragma[inline]
|
||||
private predicate invocationMatchesCallSiteFilter(Specific::InvokeNode invoke, AccessPathToken token) {
|
||||
token.getName() = "WithArity" and
|
||||
invoke.getNumArgument() = AccessPath::parseIntUnbounded(token.getAnArgument())
|
||||
or
|
||||
Specific::invocationMatchesExtraCallSiteFilter(invoke, token)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the API node identified by the first `n` tokens of `path` in the given `(package, type, path)` tuple.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
private API::Node getNodeFromPath(string package, string type, AccessPath path, int n) {
|
||||
isRelevantFullPath(package, type, path) and
|
||||
(
|
||||
n = 0 and
|
||||
exists(string package2, string type2, AccessPath path2 |
|
||||
typeModel(package, type, package2, type2, path2) and
|
||||
result = getNodeFromPath(package2, type2, path2, path2.getNumToken())
|
||||
)
|
||||
or
|
||||
// Language-specific cases, such as handling of global variables
|
||||
result = Specific::getExtraNodeFromPath(package, type, path, n)
|
||||
)
|
||||
or
|
||||
result = getSuccessorFromNode(getNodeFromPath(package, type, path, n - 1), path.getToken(n - 1))
|
||||
or
|
||||
// Similar to the other recursive case, but where the path may have stepped through one or more call-site filters
|
||||
result =
|
||||
getSuccessorFromInvoke(getInvocationFromPath(package, type, path, n - 1), path.getToken(n - 1))
|
||||
}
|
||||
|
||||
/** Gets the node identified by the given `(package, type, path)` tuple. */
|
||||
API::Node getNodeFromPath(string package, string type, AccessPath path) {
|
||||
result = getNodeFromPath(package, type, path, path.getNumToken())
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an invocation identified by the given `(package, type, path)` tuple.
|
||||
*
|
||||
* Unlike `getNodeFromPath`, the `path` may end with one or more call-site filters.
|
||||
*/
|
||||
Specific::InvokeNode getInvocationFromPath(string package, string type, AccessPath path, int n) {
|
||||
result = Specific::getAnInvocationOf(getNodeFromPath(package, type, path, n))
|
||||
or
|
||||
result = getInvocationFromPath(package, type, path, n - 1) and
|
||||
invocationMatchesCallSiteFilter(result, path.getToken(n - 1))
|
||||
}
|
||||
|
||||
/** Gets an invocation identified by the given `(package, type, path)` tuple. */
|
||||
Specific::InvokeNode getInvocationFromPath(string package, string type, AccessPath path) {
|
||||
result = getInvocationFromPath(package, type, path, path.getNumToken())
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `name` is a valid name for an access path token in the identifying access path.
|
||||
*/
|
||||
bindingset[name]
|
||||
predicate isValidTokenNameInIdentifyingAccessPath(string name) {
|
||||
name = ["Argument", "Parameter", "ReturnValue", "WithArity"]
|
||||
or
|
||||
Specific::isExtraValidTokenNameInIdentifyingAccessPath(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `name` is a valid name for an access path token with no arguments, occurring
|
||||
* in an identifying access path.
|
||||
*/
|
||||
bindingset[name]
|
||||
predicate isValidNoArgumentTokenInIdentifyingAccessPath(string name) {
|
||||
name = "ReturnValue"
|
||||
or
|
||||
Specific::isExtraValidNoArgumentTokenInIdentifyingAccessPath(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `argument` is a valid argument to an access path token with the given `name`, occurring
|
||||
* in an identifying access path.
|
||||
*/
|
||||
bindingset[name, argument]
|
||||
predicate isValidTokenArgumentInIdentifyingAccessPath(string name, string argument) {
|
||||
name = ["Argument", "Parameter"] and
|
||||
argument.regexpMatch("(N-|-)?\\d+(\\.\\.((N-|-)?\\d+)?)?")
|
||||
or
|
||||
name = "WithArity" and
|
||||
argument.regexpMatch("\\d+(\\.\\.(\\d+)?)?")
|
||||
or
|
||||
Specific::isExtraValidTokenArgumentInIdentifyingAccessPath(name, argument)
|
||||
}
|
||||
|
||||
/**
|
||||
* Module providing access to the imported models in terms of API graph nodes.
|
||||
*/
|
||||
module ModelOutput {
|
||||
/**
|
||||
* Holds if a CSV source model contributed `source` with the given `kind`.
|
||||
*/
|
||||
API::Node getASourceNode(string kind) {
|
||||
exists(string package, string type, string path |
|
||||
sourceModel(package, type, path, kind) and
|
||||
result = getNodeFromPath(package, type, path)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a CSV sink model contributed `sink` with the given `kind`.
|
||||
*/
|
||||
API::Node getASinkNode(string kind) {
|
||||
exists(string package, string type, string path |
|
||||
sinkModel(package, type, path, kind) and
|
||||
result = getNodeFromPath(package, type, path)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a relevant CSV summary exists for these parameters.
|
||||
*/
|
||||
predicate relevantSummaryModel(
|
||||
string package, string type, string path, string input, string output, string kind
|
||||
) {
|
||||
isRelevantPackage(package) and
|
||||
summaryModel(package, type, path, input, output, kind)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a `baseNode` is an invocation identified by the `package,type,path` part of a summary row.
|
||||
*/
|
||||
predicate resolvedSummaryBase(
|
||||
string package, string type, string path, Specific::InvokeNode baseNode
|
||||
) {
|
||||
summaryModel(package, type, path, _, _, _) and
|
||||
baseNode = getInvocationFromPath(package, type, path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node` is seen as an instance of `(package,type)` due to a type definition
|
||||
* contributed by a CSV model.
|
||||
*/
|
||||
API::Node getATypeNode(string package, string type) {
|
||||
exists(string package2, string type2, AccessPath path |
|
||||
typeModel(package, type, package2, type2, path) and
|
||||
result = getNodeFromPath(package2, type2, path)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an error message relating to an invalid CSV row in a model.
|
||||
*/
|
||||
string getAWarning() {
|
||||
// Check number of columns
|
||||
exists(string row, string kind, int expectedArity, int actualArity |
|
||||
any(SourceModelCsv csv).row(row) and kind = "source" and expectedArity = 4
|
||||
or
|
||||
any(SinkModelCsv csv).row(row) and kind = "sink" and expectedArity = 4
|
||||
or
|
||||
any(SummaryModelCsv csv).row(row) and kind = "summary" and expectedArity = 6
|
||||
or
|
||||
any(TypeModelCsv csv).row(row) and kind = "type" and expectedArity = 5
|
||||
|
|
||||
actualArity = count(row.indexOf(";")) + 1 and
|
||||
actualArity != expectedArity and
|
||||
result =
|
||||
"CSV " + kind + " row should have " + expectedArity + " columns but has " + actualArity +
|
||||
": " + row
|
||||
)
|
||||
or
|
||||
// Check names and arguments of access path tokens
|
||||
exists(AccessPath path, AccessPathToken token |
|
||||
isRelevantFullPath(_, _, path) and
|
||||
token = path.getToken(_)
|
||||
|
|
||||
not isValidTokenNameInIdentifyingAccessPath(token.getName()) and
|
||||
result = "Invalid token name '" + token.getName() + "' in access path: " + path
|
||||
or
|
||||
isValidTokenNameInIdentifyingAccessPath(token.getName()) and
|
||||
exists(string argument |
|
||||
argument = token.getAnArgument() and
|
||||
not isValidTokenArgumentInIdentifyingAccessPath(token.getName(), argument) and
|
||||
result =
|
||||
"Invalid argument '" + argument + "' in token '" + token + "' in access path: " + path
|
||||
)
|
||||
or
|
||||
isValidTokenNameInIdentifyingAccessPath(token.getName()) and
|
||||
token.getNumArgument() = 0 and
|
||||
not isValidNoArgumentTokenInIdentifyingAccessPath(token.getName()) and
|
||||
result = "Invalid token '" + token + "' is missing its arguments, in access path: " + path
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* Contains the language-specific part of the models-as-data implementation found in `ApiGraphModels.qll`.
|
||||
*
|
||||
* It must export the following members:
|
||||
* ```ql
|
||||
* class Unit // a unit type
|
||||
* module AccessPathSyntax // a re-export of the AccessPathSyntax module
|
||||
* class InvokeNode // a type representing an invocation connected to the API graph
|
||||
* module API // the API graph module
|
||||
* predicate isPackageUsed(string package)
|
||||
* API::Node getExtraNodeFromPath(string package, string type, string path, int n)
|
||||
* API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token)
|
||||
* API::Node getExtraSuccessorFromInvoke(API::InvokeNode node, AccessPathToken token)
|
||||
* predicate invocationMatchesExtraCallSiteFilter(API::InvokeNode invoke, AccessPathToken token)
|
||||
* InvokeNode getAnInvocationOf(API::Node node)
|
||||
* predicate isExtraValidTokenNameInIdentifyingAccessPath(string name)
|
||||
* predicate isExtraValidNoArgumentTokenInIdentifyingAccessPath(string name)
|
||||
* predicate isExtraValidTokenArgumentInIdentifyingAccessPath(string name, string argument)
|
||||
* ```
|
||||
*/
|
||||
|
||||
private import python as PY
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import ApiGraphModels
|
||||
import semmle.python.ApiGraphs::API as API
|
||||
|
||||
class Unit = PY::Unit;
|
||||
|
||||
// Re-export libraries needed by ApiGraphModels.qll
|
||||
import semmle.python.frameworks.data.internal.AccessPathSyntax as AccessPathSyntax
|
||||
private import AccessPathSyntax
|
||||
|
||||
/**
|
||||
* Holds if models describing `package` may be relevant for the analysis of this database.
|
||||
*/
|
||||
predicate isPackageUsed(string package) { exists(API::moduleImport(package)) }
|
||||
|
||||
/** Gets a Python-specific interpretation of the `(package, type, path)` tuple after resolving the first `n` access path tokens. */
|
||||
bindingset[package, type, path]
|
||||
API::Node getExtraNodeFromPath(string package, string type, AccessPath path, int n) {
|
||||
type = "" and
|
||||
n = 0 and
|
||||
result = API::moduleImport(package) and
|
||||
exists(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Python-specific API graph successor of `node` reachable by resolving `token`.
|
||||
*/
|
||||
bindingset[token]
|
||||
API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token) {
|
||||
token.getName() = "Member" and
|
||||
result = node.getMember(token.getAnArgument())
|
||||
or
|
||||
token.getName() = "Instance" and
|
||||
result = node.getReturn() // In Python `Instance` is just an alias for `ReturnValue`
|
||||
or
|
||||
token.getName() = "Awaited" and
|
||||
result = node.getAwaited()
|
||||
or
|
||||
token.getName() = "Subclass" and
|
||||
result = node.getASubclass*()
|
||||
or
|
||||
token.getName() = "Method" and
|
||||
result = node.getMember(token.getAnArgument()).getReturn()
|
||||
or
|
||||
token.getName() = ["Argument", "Parameter"] and
|
||||
(
|
||||
token.getAnArgument() = "self" and
|
||||
result = node.getSelfParameter()
|
||||
or
|
||||
exists(string name | token.getAnArgument() = name + ":" |
|
||||
result = node.getKeywordParameter(name)
|
||||
)
|
||||
or
|
||||
token.getAnArgument() = "any" and
|
||||
result = [node.getParameter(_), node.getKeywordParameter(_)]
|
||||
or
|
||||
token.getAnArgument() = "any-named" and
|
||||
result = node.getKeywordParameter(_)
|
||||
)
|
||||
// Some features don't have MaD tokens yet, they would need to be added to API-graphs first.
|
||||
// - decorators ("DecoratedClass", "DecoratedMember", "DecoratedParameter")
|
||||
// - Array/Map elements ("ArrayElement", "Element", "MapKey", "MapValue")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Python-specific API graph successor of `node` reachable by resolving `token`.
|
||||
*/
|
||||
bindingset[token]
|
||||
API::Node getExtraSuccessorFromInvoke(API::CallNode node, AccessPathToken token) {
|
||||
token.getName() = "Instance" and
|
||||
result = node.getReturn()
|
||||
or
|
||||
token.getName() = ["Argument", "Parameter"] and
|
||||
(
|
||||
token.getAnArgument() = "self" and
|
||||
result = node.getSelfParameter()
|
||||
or
|
||||
token.getAnArgument() = "any" and
|
||||
result = [node.getParameter(_), node.getKeywordParameter(_)]
|
||||
or
|
||||
token.getAnArgument() = "any-named" and
|
||||
result = node.getKeywordParameter(_)
|
||||
or
|
||||
exists(string arg | arg + ":" = token.getAnArgument() | result = node.getKeywordParameter(arg))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `invoke` matches the PY-specific call site filter in `token`.
|
||||
*/
|
||||
bindingset[token]
|
||||
predicate invocationMatchesExtraCallSiteFilter(API::CallNode invoke, AccessPathToken token) {
|
||||
token.getName() = "Call" and exists(invoke) // there is only one kind of call in Python.
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `path` is an input or output spec for a summary with the given `base` node.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
private predicate relevantInputOutputPath(API::CallNode base, AccessPath inputOrOutput) {
|
||||
exists(string package, string type, string input, string output, string path |
|
||||
ModelOutput::relevantSummaryModel(package, type, path, input, output, _) and
|
||||
ModelOutput::resolvedSummaryBase(package, type, path, base) and
|
||||
inputOrOutput = [input, output]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the API node for the first `n` tokens of the given input/output path, evaluated relative to `baseNode`.
|
||||
*/
|
||||
private API::Node getNodeFromInputOutputPath(API::CallNode baseNode, AccessPath path, int n) {
|
||||
relevantInputOutputPath(baseNode, path) and
|
||||
(
|
||||
n = 1 and
|
||||
result = getSuccessorFromInvoke(baseNode, path.getToken(0))
|
||||
or
|
||||
result =
|
||||
getSuccessorFromNode(getNodeFromInputOutputPath(baseNode, path, n - 1), path.getToken(n - 1))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the API node for the given input/output path, evaluated relative to `baseNode`.
|
||||
*/
|
||||
private API::Node getNodeFromInputOutputPath(API::CallNode baseNode, AccessPath path) {
|
||||
result = getNodeFromInputOutputPath(baseNode, path, path.getNumToken())
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a CSV summary contributed the step `pred -> succ` of the given `kind`.
|
||||
*/
|
||||
predicate summaryStep(API::Node pred, API::Node succ, string kind) {
|
||||
exists(
|
||||
string package, string type, string path, API::CallNode base, AccessPath input,
|
||||
AccessPath output
|
||||
|
|
||||
ModelOutput::relevantSummaryModel(package, type, path, input, output, kind) and
|
||||
ModelOutput::resolvedSummaryBase(package, type, path, base) and
|
||||
pred = getNodeFromInputOutputPath(base, input) and
|
||||
succ = getNodeFromInputOutputPath(base, output)
|
||||
)
|
||||
}
|
||||
|
||||
class InvokeNode = API::CallNode;
|
||||
|
||||
/** Gets an `InvokeNode` corresponding to an invocation of `node`. */
|
||||
InvokeNode getAnInvocationOf(API::Node node) { result = node.getACall() }
|
||||
|
||||
/**
|
||||
* Holds if `name` is a valid name for an access path token in the identifying access path.
|
||||
*/
|
||||
bindingset[name]
|
||||
predicate isExtraValidTokenNameInIdentifyingAccessPath(string name) {
|
||||
name = ["Member", "Instance", "Awaited", "Call", "Method", "Subclass"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `name` is a valid name for an access path token with no arguments, occurring
|
||||
* in an identifying access path.
|
||||
*/
|
||||
predicate isExtraValidNoArgumentTokenInIdentifyingAccessPath(string name) {
|
||||
name = ["Instance", "Awaited", "Call", "Subclass"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `argument` is a valid argument to an access path token with the given `name`, occurring
|
||||
* in an identifying access path.
|
||||
*/
|
||||
bindingset[name, argument]
|
||||
predicate isExtraValidTokenArgumentInIdentifyingAccessPath(string name, string argument) {
|
||||
name = ["Member", "Method"] and
|
||||
exists(argument)
|
||||
or
|
||||
name = ["Argument", "Parameter"] and
|
||||
(
|
||||
argument = ["self", "any", "any-named"]
|
||||
or
|
||||
argument.regexpMatch("\\w+:") // keyword argument
|
||||
)
|
||||
}
|
||||
@@ -4,3 +4,4 @@
|
||||
*/
|
||||
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.concepts.CryptoAlgorithms as CryptoAlgorithms
|
||||
|
||||
@@ -11,3 +11,79 @@
|
||||
*/
|
||||
|
||||
private import ConceptsImports
|
||||
|
||||
/**
|
||||
* Provides models for cryptographic concepts.
|
||||
*
|
||||
* Note: The `CryptographicAlgorithm` class currently doesn't take weak keys into
|
||||
* consideration for the `isWeak` member predicate. So RSA is always considered
|
||||
* secure, although using a low number of bits will actually make it insecure. We plan
|
||||
* to improve our libraries in the future to more precisely capture this aspect.
|
||||
*/
|
||||
module Cryptography {
|
||||
class CryptographicAlgorithm = CryptoAlgorithms::CryptographicAlgorithm;
|
||||
|
||||
class EncryptionAlgorithm = CryptoAlgorithms::EncryptionAlgorithm;
|
||||
|
||||
class HashingAlgorithm = CryptoAlgorithms::HashingAlgorithm;
|
||||
|
||||
class PasswordHashingAlgorithm = CryptoAlgorithms::PasswordHashingAlgorithm;
|
||||
|
||||
/**
|
||||
* A data-flow node that is an application of a cryptographic algorithm. For example,
|
||||
* encryption, decryption, signature-validation.
|
||||
*
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `CryptographicOperation::Range` instead.
|
||||
*/
|
||||
class CryptographicOperation extends DataFlow::Node instanceof CryptographicOperation::Range {
|
||||
/** Gets the algorithm used, if it matches a known `CryptographicAlgorithm`. */
|
||||
CryptographicAlgorithm getAlgorithm() { result = super.getAlgorithm() }
|
||||
|
||||
/** Gets an input the algorithm is used on, for example the plain text input to be encrypted. */
|
||||
DataFlow::Node getAnInput() { result = super.getAnInput() }
|
||||
|
||||
/**
|
||||
* Gets the block mode used to perform this cryptographic operation.
|
||||
* This may have no result - for example if the `CryptographicAlgorithm` used
|
||||
* is a stream cipher rather than a block cipher.
|
||||
*/
|
||||
BlockMode getBlockMode() { result = super.getBlockMode() }
|
||||
}
|
||||
|
||||
/** Provides classes for modeling new applications of a cryptographic algorithms. */
|
||||
module CryptographicOperation {
|
||||
/**
|
||||
* A data-flow node that is an application of a cryptographic algorithm. For example,
|
||||
* encryption, decryption, signature-validation.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `CryptographicOperation` instead.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node {
|
||||
/** Gets the algorithm used, if it matches a known `CryptographicAlgorithm`. */
|
||||
abstract CryptographicAlgorithm getAlgorithm();
|
||||
|
||||
/** Gets an input the algorithm is used on, for example the plain text input to be encrypted. */
|
||||
abstract DataFlow::Node getAnInput();
|
||||
|
||||
/**
|
||||
* Gets the block mode used to perform this cryptographic operation.
|
||||
* This may have no result - for example if the `CryptographicAlgorithm` used
|
||||
* is a stream cipher rather than a block cipher.
|
||||
*/
|
||||
abstract BlockMode getBlockMode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A cryptographic block cipher mode of operation. This can be used to encrypt
|
||||
* data of arbitrary length using a block encryption algorithm.
|
||||
*/
|
||||
class BlockMode extends string {
|
||||
BlockMode() { this = ["ECB", "CBC", "GCM", "CCM", "CFB", "OFB", "CTR", "OPENPGP"] }
|
||||
|
||||
/** Holds if this block mode is considered to be insecure. */
|
||||
predicate isWeak() { this = "ECB" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,12 @@ module PathInjection {
|
||||
FileSystemAccessAsSink() { this = any(FileSystemAccess e).getAPathArgument() }
|
||||
}
|
||||
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
private class DataAsFileSink extends Sink {
|
||||
DataAsFileSink() { this = ModelOutput::getASinkNode("path-injection").getARhs() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
*/
|
||||
|
||||
@@ -60,4 +60,11 @@ module SqlInjection {
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizerGuard extends SanitizerGuard, StringConstCompare { }
|
||||
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
/** A sink for sql-injection from model data. */
|
||||
private class DataAsSqlSink extends Sink {
|
||||
DataAsSqlSink() { this = ModelOutput::getASinkNode("sql-injection").getARhs() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,26 +141,28 @@ private class StatePair extends TStatePair {
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds for all constructed state pairs.
|
||||
* Holds for `(fork, fork)` state pairs when `isFork(fork, _, _, _, _)` holds.
|
||||
*
|
||||
* Used in `statePairDist`
|
||||
* Used in `statePairDistToFork`
|
||||
*/
|
||||
private predicate isStatePair(StatePair p) { any() }
|
||||
private predicate isStatePairFork(StatePair p) {
|
||||
exists(State fork | p = MkStatePair(fork, fork) and isFork(fork, _, _, _, _))
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there are transitions from the components of `q` to the corresponding
|
||||
* components of `r`.
|
||||
*
|
||||
* Used in `statePairDist`
|
||||
* Used in `statePairDistToFork`
|
||||
*/
|
||||
private predicate delta2(StatePair q, StatePair r) { step(q, _, _, r) }
|
||||
private predicate reverseStep(StatePair r, StatePair q) { step(q, _, _, r) }
|
||||
|
||||
/**
|
||||
* Gets the minimum length of a path from `q` to `r` in the
|
||||
* product automaton.
|
||||
*/
|
||||
private int statePairDist(StatePair q, StatePair r) =
|
||||
shortestDistances(isStatePair/1, delta2/2)(q, r, result)
|
||||
private int statePairDistToFork(StatePair q, StatePair r) =
|
||||
shortestDistances(isStatePairFork/1, reverseStep/2)(r, q, result)
|
||||
|
||||
/**
|
||||
* Holds if there are transitions from `q` to `r1` and from `q` to `r2`
|
||||
@@ -255,14 +257,7 @@ private predicate step(StatePair q, InputSymbol s1, InputSymbol s2, State r1, St
|
||||
|
||||
private newtype TTrace =
|
||||
Nil() or
|
||||
Step(InputSymbol s1, InputSymbol s2, TTrace t) {
|
||||
exists(StatePair p |
|
||||
isReachableFromFork(_, p, t, _) and
|
||||
step(p, s1, s2, _)
|
||||
)
|
||||
or
|
||||
t = Nil() and isFork(_, s1, s2, _, _)
|
||||
}
|
||||
Step(InputSymbol s1, InputSymbol s2, TTrace t) { isReachableFromFork(_, _, s1, s2, t, _) }
|
||||
|
||||
/**
|
||||
* A list of pairs of input symbols that describe a path in the product automaton
|
||||
@@ -284,20 +279,28 @@ private class Trace extends TTrace {
|
||||
* a path from `r` back to `(fork, fork)` with `rem` steps.
|
||||
*/
|
||||
private predicate isReachableFromFork(State fork, StatePair r, Trace w, int rem) {
|
||||
exists(InputSymbol s1, InputSymbol s2, Trace v |
|
||||
isReachableFromFork(fork, r, s1, s2, v, rem) and
|
||||
w = Step(s1, s2, v)
|
||||
)
|
||||
}
|
||||
|
||||
private predicate isReachableFromFork(
|
||||
State fork, StatePair r, InputSymbol s1, InputSymbol s2, Trace v, int rem
|
||||
) {
|
||||
// base case
|
||||
exists(InputSymbol s1, InputSymbol s2, State q1, State q2 |
|
||||
exists(State q1, State q2 |
|
||||
isFork(fork, s1, s2, q1, q2) and
|
||||
r = MkStatePair(q1, q2) and
|
||||
w = Step(s1, s2, Nil()) and
|
||||
rem = statePairDist(r, MkStatePair(fork, fork))
|
||||
v = Nil() and
|
||||
rem = statePairDistToFork(r, MkStatePair(fork, fork))
|
||||
)
|
||||
or
|
||||
// recursive case
|
||||
exists(StatePair p, Trace v, InputSymbol s1, InputSymbol s2 |
|
||||
exists(StatePair p |
|
||||
isReachableFromFork(fork, p, v, rem + 1) and
|
||||
step(p, s1, s2, r) and
|
||||
w = Step(s1, s2, v) and
|
||||
rem >= statePairDist(r, MkStatePair(fork, fork))
|
||||
rem = statePairDistToFork(r, MkStatePair(fork, fork))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,14 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
from API::CallNode call, DataFlow::Node falseyOrigin, string verb
|
||||
from
|
||||
HTTP::Client::Request request, DataFlow::Node disablingNode, DataFlow::Node origin, string ending
|
||||
where
|
||||
verb = HTTP::httpVerbLower() and
|
||||
call = API::moduleImport("requests").getMember(verb).getACall() and
|
||||
falseyOrigin = call.getKeywordParameter("verify").getAValueReachingRhs() 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"
|
||||
request.disablesCertificateValidation(disablingNode, origin) and
|
||||
// Showing the origin is only useful when it's a different node than the one disabling
|
||||
// certificate validation, for example in `requests.get(..., verify=arg)`, `arg` would
|
||||
// be the `disablingNode`, and the `origin` would be the place were `arg` got its
|
||||
// value from.
|
||||
if disablingNode = origin then ending = "." else ending = " by the value from $@."
|
||||
select request, "This request may run without certificate validation because it is $@" + ending,
|
||||
disablingNode, "disabled here", origin, "here"
|
||||
|
||||
@@ -13,13 +13,18 @@
|
||||
import python
|
||||
import semmle.python.Concepts
|
||||
|
||||
from Cryptography::CryptographicOperation operation, Cryptography::CryptographicAlgorithm algorithm
|
||||
from
|
||||
Cryptography::CryptographicOperation operation, Cryptography::CryptographicAlgorithm algorithm,
|
||||
string msgPrefix
|
||||
where
|
||||
algorithm = operation.getAlgorithm() and
|
||||
algorithm.isWeak() and
|
||||
// `Cryptography::HashingAlgorithm` and `Cryptography::PasswordHashingAlgorithm` are
|
||||
// handled by `py/weak-sensitive-data-hashing`
|
||||
algorithm instanceof Cryptography::EncryptionAlgorithm
|
||||
select operation,
|
||||
"The cryptographic algorithm " + algorithm.getName() +
|
||||
" is broken or weak, and should not be used."
|
||||
algorithm instanceof Cryptography::EncryptionAlgorithm and
|
||||
(
|
||||
algorithm.isWeak() and
|
||||
msgPrefix = "The cryptographic algorithm " + operation.getAlgorithm().getName()
|
||||
)
|
||||
or
|
||||
operation.getBlockMode().isWeak() and msgPrefix = "The block mode " + operation.getBlockMode()
|
||||
select operation, msgPrefix + " is broken or weak, and should not be used."
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.ApiGraphs
|
||||
|
||||
bindingset[p]
|
||||
int world_permission(int p) { result = p % 8 }
|
||||
@@ -33,20 +34,20 @@ string permissive_permission(int p) {
|
||||
world_permission(p) = 0 and result = "group " + access(group_permission(p))
|
||||
}
|
||||
|
||||
predicate chmod_call(CallNode call, FunctionValue chmod, NumericValue num) {
|
||||
Value::named("os.chmod") = chmod and
|
||||
chmod.getACall() = call and
|
||||
call.getArg(1).pointsTo(num)
|
||||
predicate chmod_call(API::CallNode call, string name, int mode) {
|
||||
call = API::moduleImport("os").getMember("chmod").getACall() and
|
||||
mode = call.getParameter(1, "mode").getAValueReachingRhs().asExpr().(IntegerLiteral).getValue() and
|
||||
name = "chmod"
|
||||
}
|
||||
|
||||
predicate open_call(CallNode call, FunctionValue open, NumericValue num) {
|
||||
Value::named("os.open") = open and
|
||||
open.getACall() = call and
|
||||
call.getArg(2).pointsTo(num)
|
||||
predicate open_call(API::CallNode call, string name, int mode) {
|
||||
call = API::moduleImport("os").getMember("open").getACall() and
|
||||
mode = call.getParameter(2, "mode").getAValueReachingRhs().asExpr().(IntegerLiteral).getValue() and
|
||||
name = "open"
|
||||
}
|
||||
|
||||
from CallNode call, FunctionValue func, NumericValue num, string permission
|
||||
from API::CallNode call, string name, int mode, string permission
|
||||
where
|
||||
(chmod_call(call, func, num) or open_call(call, func, num)) and
|
||||
permission = permissive_permission(num.getIntValue())
|
||||
select call, "Overly permissive mask in " + func.getName() + " sets file to " + permission + "."
|
||||
(chmod_call(call, name, mode) or open_call(call, name, mode)) and
|
||||
permission = permissive_permission(mode)
|
||||
select call, "Overly permissive mask in " + name + " sets file to " + permission + "."
|
||||
|
||||
@@ -72,7 +72,7 @@ class LgtmSuppressionComment extends LineSuppressionComment {
|
||||
* A noqa suppression comment. Both pylint and pyflakes respect this, so lgtm ought to too.
|
||||
*/
|
||||
class NoqaSuppressionComment extends LineSuppressionComment {
|
||||
NoqaSuppressionComment() { this.getContents().toLowerCase().regexpMatch("\\s*noqa\\s*") }
|
||||
NoqaSuppressionComment() { this.getContents().toLowerCase().regexpMatch("\\s*noqa\\s*([^:].*)?") }
|
||||
|
||||
override string getAnnotation() { result = "lgtm" }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* The query "Use of a broken or weak cryptographic algorithm" (`py/weak-cryptographic-algorithm`) now report if a cryptographic operation is potentially insecure due to use of a weak block mode.
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: majorAnalysis
|
||||
---
|
||||
* Improved library modeling for the query "Request without certificate validation" (`py/request-without-cert-validation`), so it now also covers `httpx`, `aiohttp.client`, and `urllib3`.
|
||||
23
python/ql/src/experimental/Security/CWE-079/ReflectedXSS.ql
Normal file
23
python/ql/src/experimental/Security/CWE-079/ReflectedXSS.ql
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @name Reflected server-side cross-site scripting
|
||||
* @description Writing user input directly to a web page
|
||||
* allows for a cross-site scripting vulnerability.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @security-severity 2.9
|
||||
* @sub-severity high
|
||||
* @id py/reflective-xss
|
||||
* @tags security
|
||||
* external/cwe/cwe-079
|
||||
* external/cwe/cwe-116
|
||||
*/
|
||||
|
||||
// determine precision above
|
||||
import python
|
||||
import experimental.semmle.python.security.dataflow.ReflectedXSS
|
||||
import DataFlow::PathGraph
|
||||
|
||||
from ReflectedXssConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where config.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink, "Cross-site scripting vulnerability due to $@.",
|
||||
source.getNode(), "a user-provided value"
|
||||
56
python/ql/src/experimental/Security/CWE-1236/CsvInjection.py
Normal file
56
python/ql/src/experimental/Security/CWE-1236/CsvInjection.py
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
"""
|
||||
@Desc :csv injection
|
||||
"""
|
||||
import copy
|
||||
import csv
|
||||
from flask import Flask
|
||||
from flask import request
|
||||
from typing import List
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/bad1')
|
||||
def bad1():
|
||||
csv_data = request.args.get('csv')
|
||||
csvWriter = csv.writer(open("test.csv", "wt"))
|
||||
csvWriter.writerow(csv_data)
|
||||
return "bad1"
|
||||
|
||||
@app.route('/good1')
|
||||
def good1():
|
||||
csv_data = request.args.get('csv')
|
||||
csvWriter = csv.writer(open("test.csv", "wt"))
|
||||
csvWriter.writerow(santize_for_csv(csv_data))
|
||||
return "good1"
|
||||
|
||||
def santize_for_csv(data: str| List[str] | List[List[str]]):
|
||||
def sanitize(item):
|
||||
return "'" + item
|
||||
|
||||
unsafe_prefixes = ("+", "=", "-", "@")
|
||||
if isinstance(data, str):
|
||||
if data.startswith(unsafe_prefixes):
|
||||
return sanitize(data)
|
||||
return data
|
||||
elif isinstance(data, list) and isinstance(data[0], str):
|
||||
sanitized_data = copy.deepcopy(data)
|
||||
for index, item in enumerate(data):
|
||||
if item.startswith(unsafe_prefixes):
|
||||
sanitized_data[index] = sanitize(item)
|
||||
return sanitized_data
|
||||
elif isinstance(data[0], list) and isinstance(data[0][0], str):
|
||||
sanitized_data = copy.deepcopy(data)
|
||||
for outer_index, sublist in enumerate(data):
|
||||
for inner_index, item in enumerate(sublist):
|
||||
if item.startswith(unsafe_prefixes):
|
||||
sanitized_data[outer_index][inner_index] = sanitize(item)
|
||||
return sanitized_data
|
||||
else:
|
||||
raise ValueError("Unsupported data type: " + str(type(data)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.debug = True
|
||||
app.run()
|
||||
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>CSV Injection, also known as Formula Injection, occurs when websites embed untrusted input inside CSV files.</p>
|
||||
<p>When a CSV format file is opened with a spreadsheet program such as Microsoft Excel or LibreOffice Calc.
|
||||
this software interprets entries beginning with <code>=</code> as formulas, which may attempt information exfiltration
|
||||
or other malicious activity when automatically executed by the spreadsheet software.</p>
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>When generating CSV output, ensure that formula-sensitive metacharacters are effectively escaped or removed from all data before storage in the resultant CSV.
|
||||
Risky characters include <code>=</code>(equal), <code>+</code>(plus), <code>-</code>(minus), and <code>@</code>(at).</p>
|
||||
|
||||
</recommendation>
|
||||
<example>
|
||||
|
||||
<p>The following examples show the bad case and the good case respectively.
|
||||
In <code>bad1</code> method, the data provided by the user is directly stored in the CSV file, which may be attacked.
|
||||
But in the <code>good1</code> method, the program will check the data provided by the user, and process the data starting with <code>=</code>(equal), <code>+</code>(plus), <code>-</code>(minus), and <code>@</code>(at) characters safely.</p>
|
||||
|
||||
<sample src="CsvInjection.py" />
|
||||
|
||||
</example>
|
||||
<references>
|
||||
<li>OWASP: <a href="https://owasp.org/www-community/attacks/CSV_Injection">CSV Injection</a>.</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
20
python/ql/src/experimental/Security/CWE-1236/CsvInjection.ql
Normal file
20
python/ql/src/experimental/Security/CWE-1236/CsvInjection.ql
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @name Csv Injection
|
||||
* @description From user-controlled data saved in CSV files, it is easy to attempt information disclosure
|
||||
* or other malicious activities when automated by spreadsheet software
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @id py/csv-injection
|
||||
* @tags security
|
||||
* external/cwe/cwe-1236
|
||||
*/
|
||||
|
||||
import python
|
||||
import DataFlow::PathGraph
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import experimental.semmle.python.security.injection.CsvInjection
|
||||
|
||||
from CsvInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where config.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink, "Csv injection might include code from $@.", source.getNode(),
|
||||
"this user input"
|
||||
@@ -371,6 +371,39 @@ class HeaderDeclaration extends DataFlow::Node {
|
||||
DataFlow::Node getValueArg() { result = range.getValueArg() }
|
||||
}
|
||||
|
||||
/** Provides classes for modeling Csv writer APIs. */
|
||||
module CsvWriter {
|
||||
/**
|
||||
* A data flow node for csv writer.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `CsvWriter` instead.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node {
|
||||
/**
|
||||
* Get the parameter value of the csv writer function.
|
||||
*/
|
||||
abstract DataFlow::Node getAnInput();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node for csv writer.
|
||||
*
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `CsvWriter::Range` instead.
|
||||
*/
|
||||
class CsvWriter extends DataFlow::Node {
|
||||
CsvWriter::Range range;
|
||||
|
||||
CsvWriter() { this = range }
|
||||
|
||||
/**
|
||||
* Get the parameter value of the csv writer function.
|
||||
*/
|
||||
DataFlow::Node getAnInput() { result = range.getAnInput() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that sets a cookie in an HTTP response.
|
||||
*
|
||||
@@ -569,3 +602,77 @@ class JwtDecoding extends DataFlow::Node instanceof JwtDecoding::Range {
|
||||
|
||||
/** DEPRECATED: Alias for JwtDecoding */
|
||||
deprecated class JWTDecoding = JwtDecoding;
|
||||
|
||||
/** Provides classes for modeling Email APIs. */
|
||||
module EmailSender {
|
||||
/**
|
||||
* A data-flow node that sends an email.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `EmailSender` instead.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node {
|
||||
/**
|
||||
* Gets a data flow node holding the plaintext version of the email body.
|
||||
*/
|
||||
abstract DataFlow::Node getPlainTextBody();
|
||||
|
||||
/**
|
||||
* Gets a data flow node holding the html version of the email body.
|
||||
*/
|
||||
abstract DataFlow::Node getHtmlBody();
|
||||
|
||||
/**
|
||||
* Gets a data flow node holding the recipients of the email.
|
||||
*/
|
||||
abstract DataFlow::Node getTo();
|
||||
|
||||
/**
|
||||
* Gets a data flow node holding the senders of the email.
|
||||
*/
|
||||
abstract DataFlow::Node getFrom();
|
||||
|
||||
/**
|
||||
* Gets a data flow node holding the subject of the email.
|
||||
*/
|
||||
abstract DataFlow::Node getSubject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that sends an email.
|
||||
*
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `EmailSender::Range` instead.
|
||||
*/
|
||||
class EmailSender extends DataFlow::Node instanceof EmailSender::Range {
|
||||
/**
|
||||
* Gets a data flow node holding the plaintext version of the email body.
|
||||
*/
|
||||
DataFlow::Node getPlainTextBody() { result = super.getPlainTextBody() }
|
||||
|
||||
/**
|
||||
* Gets a data flow node holding the html version of the email body.
|
||||
*/
|
||||
DataFlow::Node getHtmlBody() { result = super.getHtmlBody() }
|
||||
|
||||
/**
|
||||
* Gets a data flow node holding the recipients of the email.
|
||||
*/
|
||||
DataFlow::Node getTo() { result = super.getTo() }
|
||||
|
||||
/**
|
||||
* Gets a data flow node holding the senders of the email.
|
||||
*/
|
||||
DataFlow::Node getFrom() { result = super.getFrom() }
|
||||
|
||||
/**
|
||||
* Gets a data flow node holding the subject of the email.
|
||||
*/
|
||||
DataFlow::Node getSubject() { result = super.getSubject() }
|
||||
|
||||
/**
|
||||
* Gets a data flow node that refers to the HTML body or plaintext body of the email.
|
||||
*/
|
||||
DataFlow::Node getABody() { result in [super.getPlainTextBody(), super.getHtmlBody()] }
|
||||
}
|
||||
|
||||
@@ -9,8 +9,12 @@ private import experimental.semmle.python.frameworks.Werkzeug
|
||||
private import experimental.semmle.python.frameworks.LDAP
|
||||
private import experimental.semmle.python.frameworks.NoSQL
|
||||
private import experimental.semmle.python.frameworks.JWT
|
||||
private import experimental.semmle.python.frameworks.Csv
|
||||
private import experimental.semmle.python.libraries.PyJWT
|
||||
private import experimental.semmle.python.libraries.Python_JWT
|
||||
private import experimental.semmle.python.libraries.Authlib
|
||||
private import experimental.semmle.python.libraries.PythonJose
|
||||
private import experimental.semmle.python.frameworks.CopyFile
|
||||
private import experimental.semmle.python.frameworks.Sendgrid
|
||||
private import experimental.semmle.python.libraries.FlaskMail
|
||||
private import experimental.semmle.python.libraries.SmtpLib
|
||||
|
||||
73
python/ql/src/experimental/semmle/python/frameworks/Csv.qll
Normal file
73
python/ql/src/experimental/semmle/python/frameworks/Csv.qll
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `csv` PyPI package.
|
||||
* See https://docs.python.org/3/library/csv.html
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.ApiGraphs
|
||||
private import experimental.semmle.python.Concepts
|
||||
|
||||
/**
|
||||
* Provides models for the `csv` PyPI package.
|
||||
*
|
||||
* See
|
||||
* - https://docs.python.org/3/library/csv.html
|
||||
*/
|
||||
private module Csv {
|
||||
private module Writer {
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** A direct instantiation of `csv.writer` or `csv.DictWriter`. */
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CfgNode {
|
||||
ClassInstantiation() {
|
||||
this = API::moduleImport("csv").getMember(["writer", "DictWriter"]).getACall()
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets a reference to an `csv.writer` or `csv.DictWriter` instance. */
|
||||
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 `csv.writer` or `csv.DictWriter` instance. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* See:
|
||||
* - https://docs.python.org/3/library/csv.html#csvwriter.writerow
|
||||
* - https://docs.python.org/3/library/csv.html#csvwriter.writerows
|
||||
*/
|
||||
private class CsvWriteCall extends CsvWriter::Range, DataFlow::CallCfgNode {
|
||||
string methodName;
|
||||
|
||||
CsvWriteCall() {
|
||||
methodName in ["writerow", "writerows"] and
|
||||
this.(DataFlow::MethodCallNode).calls(Writer::instance(), methodName)
|
||||
}
|
||||
|
||||
override DataFlow::Node getAnInput() {
|
||||
result = this.getArg(0)
|
||||
or
|
||||
methodName = "writerow" and
|
||||
result = this.getArgByName("row")
|
||||
or
|
||||
methodName = "writerows" and
|
||||
result = this.getArgByName("rows")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See: https://docs.python.org/3/library/csv.html#csv.DictWriter
|
||||
*/
|
||||
private class DictWriterInstance extends CsvWriter::Range, DataFlow::CallCfgNode {
|
||||
DictWriterInstance() { this = API::moduleImport("csv").getMember("DictWriter").getACall() }
|
||||
|
||||
override DataFlow::Node getAnInput() {
|
||||
result in [this.getArg(1), this.getArgByName("fieldnames")]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ private import semmle.python.frameworks.Django
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import experimental.semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.Concepts
|
||||
import semmle.python.dataflow.new.RemoteFlowSources
|
||||
|
||||
private module ExperimentalPrivateDjango {
|
||||
private module DjangoMod {
|
||||
@@ -189,5 +189,90 @@ private module ExperimentalPrivateDjango {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module Email {
|
||||
/** https://docs.djangoproject.com/en/3.2/topics/email/ */
|
||||
private API::Node djangoMail() {
|
||||
result = API::moduleImport("django").getMember("core").getMember("mail")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a call to `django.core.mail.send_mail()`.
|
||||
*
|
||||
* Given the following example:
|
||||
*
|
||||
* ```py
|
||||
* send_mail("Subject", "plain-text body", "from@example.com", ["to@example.com"], html_message=django.http.request.GET.get("html"))
|
||||
* ```
|
||||
*
|
||||
* * `this` would be `send_mail("Subject", "plain-text body", "from@example.com", ["to@example.com"], html_message=django.http.request.GET.get("html"))`.
|
||||
* * `getPlainTextBody()`'s result would be `"plain-text body"`.
|
||||
* * `getHtmlBody()`'s result would be `django.http.request.GET.get("html")`.
|
||||
* * `getTo()`'s result would be `["to@example.com"]`.
|
||||
* * `getFrom()`'s result would be `"from@example.com"`.
|
||||
* * `getSubject()`'s result would be `"Subject"`.
|
||||
*/
|
||||
private class DjangoSendMail extends DataFlow::CallCfgNode, EmailSender::Range {
|
||||
DjangoSendMail() { this = djangoMail().getMember("send_mail").getACall() }
|
||||
|
||||
override DataFlow::Node getPlainTextBody() {
|
||||
result in [this.getArg(1), this.getArgByName("message")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getHtmlBody() {
|
||||
result in [this.getArg(8), this.getArgByName("html_message")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getTo() {
|
||||
result in [this.getArg(3), this.getArgByName("recipient_list")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getFrom() {
|
||||
result in [this.getArg(2), this.getArgByName("from_email")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getSubject() {
|
||||
result in [this.getArg(0), this.getArgByName("subject")]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a call to `django.core.mail.mail_admins()` or `django.core.mail.mail_managers()`.
|
||||
*
|
||||
* Given the following example:
|
||||
*
|
||||
* ```py
|
||||
* mail_admins("Subject", "plain-text body", html_message=django.http.request.GET.get("html"))
|
||||
* ```
|
||||
*
|
||||
* * `this` would be `mail_admins("Subject", "plain-text body", html_message=django.http.request.GET.get("html"))`.
|
||||
* * `getPlainTextBody()`'s result would be `"plain-text body"`.
|
||||
* * `getHtmlBody()`'s result would be `django.http.request.GET.get("html")`.
|
||||
* * `getTo()`'s result would be `none`.
|
||||
* * `getFrom()`'s result would be `none`.
|
||||
* * `getSubject()`'s result would be `"Subject"`.
|
||||
*/
|
||||
private class DjangoMailInternal extends DataFlow::CallCfgNode, EmailSender::Range {
|
||||
DjangoMailInternal() {
|
||||
this = djangoMail().getMember(["mail_admins", "mail_managers"]).getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getPlainTextBody() {
|
||||
result in [this.getArg(1), this.getArgByName("message")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getHtmlBody() {
|
||||
result in [this.getArg(4), this.getArgByName("html_message")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getTo() { none() }
|
||||
|
||||
override DataFlow::Node getFrom() { none() }
|
||||
|
||||
override DataFlow::Node getSubject() {
|
||||
result in [this.getArg(0), this.getArgByName("subject")]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
187
python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll
Normal file
187
python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `sendgrid` PyPI package.
|
||||
* See https://github.com/sendgrid/sendgrid-python.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import experimental.semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
private module Sendgrid {
|
||||
/** Gets a reference to the `sendgrid` module. */
|
||||
private API::Node sendgrid() { result = API::moduleImport("sendgrid") }
|
||||
|
||||
/** Gets a reference to `sendgrid.helpers.mail` */
|
||||
private API::Node sendgridMailHelper() {
|
||||
result = sendgrid().getMember("helpers").getMember("mail")
|
||||
}
|
||||
|
||||
/** Gets a reference to `sendgrid.helpers.mail.Mail` */
|
||||
private API::Node sendgridMailInstance() { result = sendgridMailHelper().getMember("Mail") }
|
||||
|
||||
/** Gets a reference to a `SendGridAPIClient` instance. */
|
||||
private API::Node sendgridApiClient() {
|
||||
result = sendgrid().getMember("SendGridAPIClient").getReturn()
|
||||
}
|
||||
|
||||
/** Gets a reference to a `SendGridAPIClient` instance call with `send` or `post`. */
|
||||
private DataFlow::CallCfgNode sendgridApiSendCall() {
|
||||
result = sendgridApiClient().getMember("send").getACall()
|
||||
or
|
||||
result =
|
||||
sendgridApiClient()
|
||||
.getMember("client")
|
||||
.getMember("mail")
|
||||
.getMember("send")
|
||||
.getMember("post")
|
||||
.getACall()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reference to `sg.send()` and `sg.client.mail.send.post()`.
|
||||
*
|
||||
* Given the following example:
|
||||
*
|
||||
* ```py
|
||||
* from_email = Email("from@example.com")
|
||||
* to_email = To("to@example.com")
|
||||
* subject = "Sending with SendGrid is Fun"
|
||||
* content = Content("text/html", request.args["html_content"])
|
||||
*
|
||||
* mail = Mail(from_email, to_email, subject, content)
|
||||
*
|
||||
* sg = SendGridAPIClient(api_key='SENDGRID_API_KEY')
|
||||
* response = sg.client.mail.send.post(request_body=mail.get())
|
||||
* ```
|
||||
*
|
||||
* * `this` would be `sg.client.mail.send.post(request_body=mail.get())`.
|
||||
* * `getPlainTextBody()`'s result would be `none()`.
|
||||
* * `getHtmlBody()`'s result would be `request.args["html_content"]`.
|
||||
* * `getTo()`'s result would be `"to@example.com"`.
|
||||
* * `getFrom()`'s result would be `"from@example.com"`.
|
||||
* * `getSubject()`'s result would be `"Sending with SendGrid is Fun"`.
|
||||
*/
|
||||
private class SendGridMail extends DataFlow::CallCfgNode, EmailSender::Range {
|
||||
SendGridMail() { this = sendgridApiSendCall() }
|
||||
|
||||
private DataFlow::CallCfgNode getMailCall() {
|
||||
exists(DataFlow::Node n |
|
||||
n in [this.getArg(0), this.getArgByName("request_body")] and
|
||||
result = [n, n.(DataFlow::MethodCallNode).getObject()].getALocalSource()
|
||||
)
|
||||
}
|
||||
|
||||
private DataFlow::Node sendgridContent(DataFlow::CallCfgNode contentCall, string mime) {
|
||||
mime in ["text/plain", "text/html", "text/x-amp-html"] and
|
||||
exists(StrConst mimeNode |
|
||||
mimeNode.getText() = mime and
|
||||
DataFlow::exprNode(mimeNode).(DataFlow::LocalSourceNode).flowsTo(contentCall.getArg(0)) and
|
||||
result = contentCall.getArg(1)
|
||||
)
|
||||
}
|
||||
|
||||
private DataFlow::Node sendgridWrite(string attributeName) {
|
||||
attributeName in ["plain_text_content", "html_content", "from_email", "subject"] and
|
||||
exists(DataFlow::AttrWrite attrWrite |
|
||||
attrWrite.getObject().getALocalSource() = this.getMailCall() and
|
||||
attrWrite.getAttributeName() = attributeName and
|
||||
result = attrWrite.getValue()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getPlainTextBody() {
|
||||
result in [
|
||||
this.getMailCall().getArg(3), this.getMailCall().getArgByName("plain_text_content")
|
||||
]
|
||||
or
|
||||
result in [
|
||||
this.sendgridContent([
|
||||
this.getMailCall().getArg(3), this.getMailCall().getArgByName("plain_text_content")
|
||||
].getALocalSource(), "text/plain"),
|
||||
this.sendgridContent(sendgridMailInstance().getMember("add_content").getACall(),
|
||||
"text/plain")
|
||||
]
|
||||
or
|
||||
result = this.sendgridWrite("plain_text_content")
|
||||
}
|
||||
|
||||
override DataFlow::Node getHtmlBody() {
|
||||
result in [this.getMailCall().getArg(4), this.getMailCall().getArgByName("html_content")]
|
||||
or
|
||||
result = this.getMailCall().getAMethodCall("set_html").getArg(0)
|
||||
or
|
||||
result =
|
||||
this.sendgridContent([
|
||||
this.getMailCall().getArg(4), this.getMailCall().getArgByName("html_content")
|
||||
].getALocalSource(), ["text/html", "text/x-amp-html"])
|
||||
or
|
||||
result = this.sendgridWrite("html_content")
|
||||
or
|
||||
exists(KeyValuePair content, Dict generalDict, KeyValuePair typePair, KeyValuePair valuePair |
|
||||
content.getKey().(StrConst).getText() = "content" and
|
||||
content.getValue().(List).getAnElt() = generalDict and
|
||||
// declare KeyValuePairs keys and values
|
||||
typePair.getKey().(StrConst).getText() = "type" and
|
||||
typePair.getValue().(StrConst).getText() = ["text/html", "text/x-amp-html"] and
|
||||
valuePair.getKey().(StrConst).getText() = "value" and
|
||||
result.asExpr() = valuePair.getValue() and
|
||||
// correlate generalDict with previously set KeyValuePairs
|
||||
generalDict.getAnItem() in [typePair, valuePair] and
|
||||
[this.getArg(0), this.getArgByName("request_body")].getALocalSource().asExpr() =
|
||||
any(Dict d | d.getAnItem() = content)
|
||||
)
|
||||
or
|
||||
exists(KeyValuePair footer, Dict generalDict, KeyValuePair enablePair, KeyValuePair htmlPair |
|
||||
footer.getKey().(StrConst).getText() = ["footer", "subscription_tracking"] and
|
||||
footer.getValue() = generalDict and
|
||||
// check footer is enabled
|
||||
enablePair.getKey().(StrConst).getText() = "enable" and
|
||||
exists(enablePair.getValue().(True)) and
|
||||
// get html content
|
||||
htmlPair.getKey().(StrConst).getText() = "html" and
|
||||
result.asExpr() = htmlPair.getValue() and
|
||||
// correlate generalDict with previously set KeyValuePairs
|
||||
generalDict.getAnItem() in [enablePair, htmlPair] and
|
||||
exists(KeyValuePair k |
|
||||
k.getKey() =
|
||||
[this.getArg(0), this.getArgByName("request_body")]
|
||||
.getALocalSource()
|
||||
.asExpr()
|
||||
.(Dict)
|
||||
.getAKey() and
|
||||
k.getValue() = any(Dict d | d.getAKey() = footer.getKey())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getTo() {
|
||||
result in [this.getMailCall().getArg(1), this.getMailCall().getArgByName("to_emails")]
|
||||
or
|
||||
result = this.getMailCall().getAMethodCall("To").getArg(0)
|
||||
or
|
||||
result =
|
||||
this.getMailCall()
|
||||
.getAMethodCall(["to", "add_to", "cc", "add_cc", "bcc", "add_bcc"])
|
||||
.getArg(0)
|
||||
}
|
||||
|
||||
override DataFlow::Node getFrom() {
|
||||
result in [this.getMailCall().getArg(0), this.getMailCall().getArgByName("from_email")]
|
||||
or
|
||||
result = this.getMailCall().getAMethodCall("Email").getArg(0)
|
||||
or
|
||||
result = this.getMailCall().getAMethodCall(["from_email", "set_from"]).getArg(0)
|
||||
or
|
||||
result = this.sendgridWrite("from_email")
|
||||
}
|
||||
|
||||
override DataFlow::Node getSubject() {
|
||||
result in [this.getMailCall().getArg(2), this.getMailCall().getArgByName("subject")]
|
||||
or
|
||||
result = this.getMailCall().getAMethodCall(["subject", "set_subject"]).getArg(0)
|
||||
or
|
||||
result = this.sendgridWrite("subject")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `flask` PyPI package.
|
||||
* See https://flask.palletsprojects.com/en/1.1.x/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import experimental.semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/** https://pythonhosted.org/Flask-Mail/#module-flask_mail */
|
||||
private module FlaskMail {
|
||||
/** Gets a reference to `flask_mail`, `flask_sendmail` and `flask.ext.sendmail`. */
|
||||
private API::Node flaskMail() {
|
||||
result = API::moduleImport(["flask_mail", "flask_sendmail", "flask.ext.sendmail"])
|
||||
}
|
||||
|
||||
/** Gets a reference to `flask_mail.Mail()`, `flask_sendmail.Mail()` and `flask.ext.sendmail.Mail()`. */
|
||||
private API::Node flaskMailInstance() { result = flaskMail().getMember("Mail").getReturn() }
|
||||
|
||||
/**
|
||||
* Gets a call to `mail.send()`.
|
||||
*
|
||||
* Given the following example:
|
||||
*
|
||||
* ```py
|
||||
* msg = Message(subject="Subject",
|
||||
* sender="from@example.com",
|
||||
* recipients=["to@example.com"],
|
||||
* body="plain-text body",
|
||||
* html=request.args["html"])
|
||||
* mail.send(msg)
|
||||
* ```
|
||||
*
|
||||
* * `this` would be `mail.send(msg)`.
|
||||
* * `getPlainTextBody()`'s result would be `"plain-text body"`.
|
||||
* * `getHtmlBody()`'s result would be `request.args["html"]`.
|
||||
* * `getTo()`'s result would be `["to@example.com"]`.
|
||||
* * `getFrom()`'s result would be `"from@example.com"`.
|
||||
* * `getSubject()`'s result would be `"Subject"`.
|
||||
*/
|
||||
private class FlaskMail extends DataFlow::CallCfgNode, EmailSender::Range {
|
||||
FlaskMail() {
|
||||
this =
|
||||
[flaskMailInstance(), flaskMailInstance().getMember("connect").getReturn()]
|
||||
.getMember(["send", "send_message"])
|
||||
.getACall()
|
||||
}
|
||||
|
||||
private DataFlow::CallCfgNode getMessage() { result = this.getArg(0).getALocalSource() }
|
||||
|
||||
bindingset[argumentPosition]
|
||||
private DataFlow::Node getFlaskMailArgument(int argumentPosition, string argumentName) {
|
||||
argumentPosition in [[0 .. 3], 5] and
|
||||
argumentName in ["body", "html", "recipients", "sender", "subject"] and
|
||||
result in [
|
||||
this.getMessage().getArg(argumentPosition), this.getMessage().getArgByName(argumentName)
|
||||
]
|
||||
or
|
||||
exists(DataFlow::AttrWrite write |
|
||||
write.getObject().getALocalSource() = this.getMessage() and
|
||||
write.getAttributeName() = argumentName and
|
||||
result = write.getValue()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getPlainTextBody() { result = this.getFlaskMailArgument(2, "body") }
|
||||
|
||||
override DataFlow::Node getHtmlBody() { result = this.getFlaskMailArgument(3, "html") }
|
||||
|
||||
override DataFlow::Node getTo() {
|
||||
result = this.getFlaskMailArgument(1, "recipients")
|
||||
or
|
||||
result = this.getMessage().getAMethodCall("add_recipient").getACall().getArg(0)
|
||||
}
|
||||
|
||||
override DataFlow::Node getFrom() { result = this.getFlaskMailArgument(5, "sender") }
|
||||
|
||||
override DataFlow::Node getSubject() { result = this.getFlaskMailArgument(0, "subject") }
|
||||
}
|
||||
}
|
||||
177
python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll
Normal file
177
python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll
Normal file
@@ -0,0 +1,177 @@
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import experimental.semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.dataflow.new.TaintTracking2
|
||||
|
||||
module SmtpLib {
|
||||
/** Gets a reference to `smtplib.SMTP_SSL` */
|
||||
private API::Node smtpConnectionInstance() {
|
||||
result = API::moduleImport("smtplib").getMember("SMTP_SSL")
|
||||
}
|
||||
|
||||
/** Gets a reference to `email.mime.multipart.MIMEMultipart` */
|
||||
private API::Node smtpMimeMultipartInstance() {
|
||||
result =
|
||||
API::moduleImport("email").getMember("mime").getMember("multipart").getMember("MIMEMultipart")
|
||||
}
|
||||
|
||||
/** Gets a reference to `email.mime.text.MIMEText` */
|
||||
private API::Node smtpMimeTextInstance() {
|
||||
result = API::moduleImport("email").getMember("mime").getMember("text").getMember("MIMEText")
|
||||
}
|
||||
|
||||
private DataFlow::CallCfgNode mimeText(string mimetype) {
|
||||
result = smtpMimeTextInstance().getACall() and
|
||||
[result.getArg(1), result.getArgByName("_subtype")].asExpr().(StrConst).getText() = mimetype
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets flow from `MIMEText()` to `MIMEMultipart(_subparts=(part1, part2))`'s `_subparts`
|
||||
* argument. Used because of the impossibility to get local source nodes from `_subparts`'
|
||||
* `(List|Tuple)` elements.
|
||||
*/
|
||||
private class SMTPMessageConfig extends TaintTracking2::Configuration {
|
||||
SMTPMessageConfig() { this = "SMTPMessageConfig" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source = mimeText(_) }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) {
|
||||
sink = smtpMimeMultipartInstance().getACall().getArgByName("_subparts")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the `MimeText` call retrieves the content argument whose type argument equals `mimetype`.
|
||||
* This call flows into `MIMEMultipart`'s `_subparts` argument or the `.attach()` method call
|
||||
* and both local source nodes correlate to `smtp`'s `sendmail` call 3rd argument's local source.
|
||||
*
|
||||
* Given the following example with `getSmtpMessage(any(SmtpLibSendMail s), "html")`:
|
||||
*
|
||||
* ```py
|
||||
* part1 = MIMEText(text, "plain")
|
||||
* part2 = MIMEText(html, "html")
|
||||
* message = MIMEMultipart(_subparts=(part1, part2))
|
||||
* server.sendmail(sender_email, receiver_email, message.as_string())
|
||||
* ```
|
||||
*
|
||||
* * `source` would be `MIMEText(text, "html")`.
|
||||
* * `sink` would be `MIMEMultipart(_subparts=(part1, part2))`.
|
||||
* * Then `message` local source node is correlated to `sink`.
|
||||
* * Then the flow from `source` to `_subparts` is checked.
|
||||
*
|
||||
* Given the following example with `getSmtpMessage(any(SmtpLibSendMail s), "html")`:
|
||||
*
|
||||
* ```py
|
||||
* part1 = MIMEText(text, "plain")
|
||||
* part2 = MIMEText(html, "html")
|
||||
* message = MIMEMultipart("alternative")
|
||||
* message.attach(part1)
|
||||
* message.attach(part2)
|
||||
* server.sendmail(sender_email, receiver_email, message.as_string())
|
||||
* ```
|
||||
*
|
||||
* * `source` would be `MIMEText(text, "html")`.
|
||||
* * `sink` would be `message.attach(part2)`.
|
||||
* * Then `sink`'s object (`message`) local source is correlated to `server.sendmail`
|
||||
* 3rd argument local source (`MIMEMultipart("alternative")`).
|
||||
* * Then the flow from `source` to `sink` 1st argument is checked.
|
||||
*/
|
||||
bindingset[mimetype]
|
||||
private DataFlow::Node getSmtpMessage(DataFlow::CallCfgNode sendCall, string mimetype) {
|
||||
exists(DataFlow::Node source, DataFlow::Node sink |
|
||||
source = mimeText(mimetype) and
|
||||
(
|
||||
// via _subparts
|
||||
sink = smtpMimeMultipartInstance().getACall() and
|
||||
sink =
|
||||
[sendCall.getArg(2), sendCall.getArg(2).(DataFlow::MethodCallNode).getObject()]
|
||||
.getALocalSource() and
|
||||
any(SMTPMessageConfig a)
|
||||
.hasFlow(source, sink.(DataFlow::CallCfgNode).getArgByName("_subparts"))
|
||||
or
|
||||
// via .attach()
|
||||
sink = smtpMimeMultipartInstance().getReturn().getMember("attach").getACall() and
|
||||
sink.(DataFlow::MethodCallNode).getObject().getALocalSource() =
|
||||
[sendCall.getArg(2), sendCall.getArg(2).(DataFlow::MethodCallNode).getObject()]
|
||||
.getALocalSource() and
|
||||
source.(DataFlow::CallCfgNode).flowsTo(sink.(DataFlow::CallCfgNode).getArg(0))
|
||||
) and
|
||||
result = source.(DataFlow::CallCfgNode).getArg(0)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a message subscript write by correlating subscript's object local source with
|
||||
* `smtp`'s `sendmail` call 3rd argument's local source.
|
||||
*
|
||||
* Given the following example with `getSMTPSubscriptByIndex(any(SmtpLibSendMail s), "Subject")`:
|
||||
*
|
||||
* ```py
|
||||
* message = MIMEMultipart("alternative")
|
||||
* message["Subject"] = "multipart test"
|
||||
* server.sendmail(sender_email, receiver_email, message.as_string())
|
||||
* ```
|
||||
*
|
||||
* * `def` would be `message["Subject"]` (`DefinitionNode`)
|
||||
* * `sub` would be `message["Subject"]` (`Subscript`)
|
||||
* * `result` would be `"multipart test"`
|
||||
*/
|
||||
private DataFlow::Node getSMTPSubscriptByIndex(DataFlow::CallCfgNode sendCall, string index) {
|
||||
exists(DefinitionNode def, Subscript sub |
|
||||
sub = def.getNode() and
|
||||
DataFlow::exprNode(sub.getObject()).getALocalSource() =
|
||||
[sendCall.getArg(2), sendCall.getArg(2).(DataFlow::MethodCallNode).getObject()]
|
||||
.getALocalSource() and
|
||||
sub.getIndex().(StrConst).getText() = index and
|
||||
result.asCfgNode() = def.getValue()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reference to `smtplib.SMTP_SSL().sendmail()`.
|
||||
*
|
||||
* Given the following example:
|
||||
*
|
||||
* ```py
|
||||
* part1 = MIMEText(text, "plain")
|
||||
* part2 = MIMEText(html, "html")
|
||||
*
|
||||
* message = MIMEMultipart(_subparts=(part1, part2))
|
||||
* message["Subject"] = "multipart test"
|
||||
* message["From"] = sender_email
|
||||
* message["To"] = receiver_email
|
||||
*
|
||||
* server.login(sender_email, "SERVER_PASSWORD")
|
||||
* server.sendmail(sender_email, receiver_email, message.as_string())
|
||||
* ```
|
||||
*
|
||||
* * `this` would be `server.sendmail(sender_email, receiver_email, message.as_string())`.
|
||||
* * `getPlainTextBody()`'s result would be `text`.
|
||||
* * `getHtmlBody()`'s result would be `html`.
|
||||
* * `getTo()`'s result would be `receiver_email`.
|
||||
* * `getFrom()`'s result would be `sender_email`.
|
||||
* * `getSubject()`'s result would be `"multipart test"`.
|
||||
*/
|
||||
private class SmtpLibSendMail extends DataFlow::CallCfgNode, EmailSender::Range {
|
||||
SmtpLibSendMail() {
|
||||
this = smtpConnectionInstance().getReturn().getMember("sendmail").getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getPlainTextBody() { result = getSmtpMessage(this, "plain") }
|
||||
|
||||
override DataFlow::Node getHtmlBody() { result = getSmtpMessage(this, "html") }
|
||||
|
||||
override DataFlow::Node getTo() {
|
||||
result in [this.getArg(1), getSMTPSubscriptByIndex(this, "To")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getFrom() {
|
||||
result in [this.getArg(0), getSMTPSubscriptByIndex(this, "From")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getSubject() {
|
||||
result in [this.getArg(2), getSMTPSubscriptByIndex(this, "Subject")]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Provides a taint-tracking configuration for detecting reflected server-side
|
||||
* cross-site scripting vulnerabilities.
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import semmle.python.dataflow.new.RemoteFlowSources
|
||||
import semmle.python.dataflow.new.BarrierGuards
|
||||
import experimental.semmle.python.Concepts
|
||||
import semmle.python.Concepts
|
||||
import semmle.python.ApiGraphs
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for detecting reflected server-side cross-site
|
||||
* scripting vulnerabilities.
|
||||
*/
|
||||
class ReflectedXssConfiguration extends TaintTracking::Configuration {
|
||||
ReflectedXssConfiguration() { this = "ReflectedXssConfiguration" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink = any(EmailSender email).getHtmlBody() }
|
||||
|
||||
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
|
||||
guard instanceof StringConstCompare
|
||||
}
|
||||
|
||||
override predicate isSanitizer(DataFlow::Node sanitizer) {
|
||||
sanitizer = any(HtmlEscaping esc).getOutput()
|
||||
}
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
exists(DataFlow::CallCfgNode htmlContentCall |
|
||||
htmlContentCall =
|
||||
API::moduleImport("sendgrid")
|
||||
.getMember("helpers")
|
||||
.getMember("mail")
|
||||
.getMember("HtmlContent")
|
||||
.getACall() and
|
||||
nodeTo = htmlContentCall and
|
||||
nodeFrom = htmlContentCall.getArg(0)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import python
|
||||
import experimental.semmle.python.Concepts
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import semmle.python.dataflow.new.BarrierGuards
|
||||
import semmle.python.dataflow.new.RemoteFlowSources
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for tracking untrusted user input used in file read.
|
||||
*/
|
||||
class CsvInjectionFlowConfig extends TaintTracking::Configuration {
|
||||
CsvInjectionFlowConfig() { this = "CsvInjectionFlowConfig" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink = any(CsvWriter cw).getAnInput() }
|
||||
|
||||
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
|
||||
guard instanceof StartsWithCheck or
|
||||
guard instanceof StringConstCompare
|
||||
}
|
||||
}
|
||||
|
||||
private class StartsWithCheck extends DataFlow::BarrierGuard {
|
||||
DataFlow::MethodCallNode mc;
|
||||
|
||||
StartsWithCheck() {
|
||||
this = mc.asCfgNode() and
|
||||
mc.calls(_, "startswith")
|
||||
}
|
||||
|
||||
override predicate checks(ControlFlowNode node, boolean branch) {
|
||||
node = mc.getObject().asCfgNode() and
|
||||
branch = true
|
||||
}
|
||||
}
|
||||
@@ -239,12 +239,24 @@ private string getColumnString(TColumn column) {
|
||||
|
||||
/**
|
||||
* RegEx pattern to match a single expected result, not including the leading `$`. It consists of one or
|
||||
* more comma-separated tags containing only letters, digits, `-` and `_` (note that the first character
|
||||
* must not be a digit), optionally followed by `=` and the expected value.
|
||||
* more comma-separated tags optionally followed by `=` and the expected value.
|
||||
*
|
||||
* Tags must be only letters, digits, `-` and `_` (note that the first character
|
||||
* must not be a digit), but can contain anything enclosed in a single set of
|
||||
* square brackets.
|
||||
*
|
||||
* Examples:
|
||||
* - `tag`
|
||||
* - `tag=value`
|
||||
* - `tag,tag2=value`
|
||||
* - `tag[foo bar]=value`
|
||||
*
|
||||
* Not allowed:
|
||||
* - `tag[[[foo bar]`
|
||||
*/
|
||||
private string expectationPattern() {
|
||||
exists(string tag, string tags, string value |
|
||||
tag = "[A-Za-z-_][A-Za-z-_0-9]*" and
|
||||
tag = "[A-Za-z-_](?:[A-Za-z-_0-9]|\\[[^\\]\\]]*\\])*" and
|
||||
tags = "((?:" + tag + ")(?:\\s*,\\s*" + tag + ")*)" and
|
||||
// In Python, we allow both `"` and `'` for strings, as well as the prefixes `bru`.
|
||||
// For example, `b"foo"`.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:1:5:1:17 | GSSA Variable obfuscated_id |
|
||||
| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:1:5:1:17 | GSSA Variable obfuscated_id |
|
||||
| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:7:5:7:17 | ControlFlowNode for obfuscated_id |
|
||||
| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:7:5:7:17 | ControlFlowNode for obfuscated_id |
|
||||
| test.py:1:5:1:17 | GSSA Variable obfuscated_id | test.py:7:5:7:17 | ControlFlowNode for obfuscated_id |
|
||||
| test.py:1:5:1:17 | GSSA Variable obfuscated_id | test.py:7:5:7:17 | ControlFlowNode for obfuscated_id |
|
||||
| test.py:1:19:1:19 | ControlFlowNode for x | test.py:1:19:1:19 | SSA variable x |
|
||||
| test.py:1:19:1:19 | ControlFlowNode for x | test.py:1:19:1:19 | SSA variable x |
|
||||
@@ -97,7 +95,6 @@
|
||||
| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:7:19:7:19 | ControlFlowNode for a |
|
||||
| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:7:19:7:19 | ControlFlowNode for a |
|
||||
| test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | test.py:7:1:7:1 | GSSA Variable b |
|
||||
| test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | test.py:7:1:7:1 | GSSA Variable b |
|
||||
| test.py:7:19:7:19 | ControlFlowNode for a | test.py:1:19:1:19 | ControlFlowNode for x |
|
||||
| test.py:7:19:7:19 | ControlFlowNode for a | test.py:1:19:1:19 | ControlFlowNode for x |
|
||||
| test.py:7:19:7:19 | ControlFlowNode for a | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() |
|
||||
|
||||
@@ -487,7 +487,8 @@ class CryptographicOperationTest extends InlineExpectationsTest {
|
||||
|
||||
override string getARelevantTag() {
|
||||
result in [
|
||||
"CryptographicOperation", "CryptographicOperationInput", "CryptographicOperationAlgorithm"
|
||||
"CryptographicOperation", "CryptographicOperationInput", "CryptographicOperationAlgorithm",
|
||||
"CryptographicOperationBlockMode"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -507,6 +508,10 @@ class CryptographicOperationTest extends InlineExpectationsTest {
|
||||
element = cryptoOperation.toString() and
|
||||
value = cryptoOperation.getAlgorithm().getName() and
|
||||
tag = "CryptographicOperationAlgorithm"
|
||||
or
|
||||
element = cryptoOperation.toString() and
|
||||
value = cryptoOperation.getBlockMode() and
|
||||
tag = "CryptographicOperationBlockMode"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
48
python/ql/test/experimental/meta/MaDTest.qll
Normal file
48
python/ql/test/experimental/meta/MaDTest.qll
Normal file
@@ -0,0 +1,48 @@
|
||||
import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.dataflow.new.internal.PrintNode
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
// need to import Frameworks to get the actual modeling imported
|
||||
private import semmle.python.Frameworks
|
||||
// this import needs to be public to get the query predicates propagated to the actual test files
|
||||
import TestUtilities.InlineExpectationsTest
|
||||
|
||||
class MadSinkTest extends InlineExpectationsTest {
|
||||
MadSinkTest() { this = "MadSinkTest" }
|
||||
|
||||
override string getARelevantTag() {
|
||||
exists(string kind | exists(ModelOutput::getASinkNode(kind)) |
|
||||
result = "mad-sink[" + kind + "]"
|
||||
)
|
||||
}
|
||||
|
||||
override predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(location.getFile().getRelativePath()) and
|
||||
exists(DataFlow::Node sink, string kind |
|
||||
sink = ModelOutput::getASinkNode(kind).getARhs() and
|
||||
location = sink.getLocation() and
|
||||
element = sink.toString() and
|
||||
value = prettyNodeForInlineTest(sink) and
|
||||
tag = "mad-sink[" + kind + "]"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class MadSourceTest extends InlineExpectationsTest {
|
||||
MadSourceTest() { this = "MadSourceTest" }
|
||||
|
||||
override string getARelevantTag() {
|
||||
exists(string kind | exists(ModelOutput::getASourceNode(kind)) | result = "mad-source__" + kind)
|
||||
}
|
||||
|
||||
override predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(location.getFile().getRelativePath()) and
|
||||
exists(DataFlow::Node source, string kind |
|
||||
source = ModelOutput::getASourceNode(kind).getAnImmediateUse() and
|
||||
location = source.getLocation() and
|
||||
element = source.toString() and
|
||||
value = prettyNodeForInlineTest(source) and
|
||||
tag = "mad-source__" + kind
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
edges
|
||||
| flask_mail.py:13:22:13:28 | ControlFlowNode for request | flask_mail.py:13:22:13:33 | ControlFlowNode for Attribute |
|
||||
| flask_mail.py:13:22:13:28 | ControlFlowNode for request | flask_mail.py:18:14:18:25 | ControlFlowNode for Attribute |
|
||||
| flask_mail.py:13:22:13:33 | ControlFlowNode for Attribute | flask_mail.py:13:22:13:41 | ControlFlowNode for Subscript |
|
||||
| flask_mail.py:18:14:18:20 | ControlFlowNode for request | flask_mail.py:18:14:18:25 | ControlFlowNode for Attribute |
|
||||
| flask_mail.py:18:14:18:25 | ControlFlowNode for Attribute | flask_mail.py:18:14:18:33 | ControlFlowNode for Subscript |
|
||||
| flask_mail.py:31:24:31:30 | ControlFlowNode for request | flask_mail.py:31:24:31:35 | ControlFlowNode for Attribute |
|
||||
| flask_mail.py:31:24:31:35 | ControlFlowNode for Attribute | flask_mail.py:31:24:31:43 | ControlFlowNode for Subscript |
|
||||
| sendgrid_mail.py:14:22:14:28 | ControlFlowNode for request | sendgrid_mail.py:14:22:14:33 | ControlFlowNode for Attribute |
|
||||
| sendgrid_mail.py:14:22:14:33 | ControlFlowNode for Attribute | sendgrid_mail.py:14:22:14:49 | ControlFlowNode for Subscript |
|
||||
| sendgrid_mail.py:26:34:26:40 | ControlFlowNode for request | sendgrid_mail.py:26:34:26:45 | ControlFlowNode for Attribute |
|
||||
| sendgrid_mail.py:26:34:26:45 | ControlFlowNode for Attribute | sendgrid_mail.py:26:34:26:61 | ControlFlowNode for Subscript |
|
||||
| sendgrid_mail.py:26:34:26:61 | ControlFlowNode for Subscript | sendgrid_mail.py:26:22:26:62 | ControlFlowNode for HtmlContent() |
|
||||
| sendgrid_mail.py:37:41:37:47 | ControlFlowNode for request | sendgrid_mail.py:37:41:37:52 | ControlFlowNode for Attribute |
|
||||
| sendgrid_mail.py:37:41:37:52 | ControlFlowNode for Attribute | sendgrid_mail.py:37:41:37:68 | ControlFlowNode for Subscript |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:62 | ControlFlowNode for Attribute |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:61 | ControlFlowNode for Attribute |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:61 | ControlFlowNode for Attribute |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:62 | ControlFlowNode for Attribute | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:78 | ControlFlowNode for Subscript |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:78 | ControlFlowNode for Subscript | sendgrid_via_mail_send_post_request_body_bad.py:16:26:16:79 | ControlFlowNode for Attribute() |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:56 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:61 | ControlFlowNode for Attribute |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:56 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:61 | ControlFlowNode for Attribute |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:61 | ControlFlowNode for Attribute | sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:76 | ControlFlowNode for Subscript |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:76 | ControlFlowNode for Subscript | sendgrid_via_mail_send_post_request_body_bad.py:27:25:27:77 | ControlFlowNode for Attribute() |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:56 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:61 | ControlFlowNode for Attribute |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:61 | ControlFlowNode for Attribute | sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:78 | ControlFlowNode for Subscript |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:78 | ControlFlowNode for Subscript | sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() |
|
||||
| smtplib_bad_subparts.py:17:12:17:18 | ControlFlowNode for request | smtplib_bad_subparts.py:17:12:17:23 | ControlFlowNode for Attribute |
|
||||
| smtplib_bad_subparts.py:17:12:17:23 | ControlFlowNode for Attribute | smtplib_bad_subparts.py:17:12:17:33 | ControlFlowNode for Subscript |
|
||||
| smtplib_bad_subparts.py:17:12:17:33 | ControlFlowNode for Subscript | smtplib_bad_subparts.py:24:22:24:25 | ControlFlowNode for html |
|
||||
| smtplib_bad_via_attach.py:20:12:20:18 | ControlFlowNode for request | smtplib_bad_via_attach.py:20:12:20:23 | ControlFlowNode for Attribute |
|
||||
| smtplib_bad_via_attach.py:20:12:20:23 | ControlFlowNode for Attribute | smtplib_bad_via_attach.py:20:12:20:31 | ControlFlowNode for Subscript |
|
||||
| smtplib_bad_via_attach.py:20:12:20:31 | ControlFlowNode for Subscript | smtplib_bad_via_attach.py:27:22:27:25 | ControlFlowNode for html |
|
||||
nodes
|
||||
| django_mail.py:14:48:14:82 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| django_mail.py:23:30:23:64 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| django_mail.py:25:32:25:66 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| flask_mail.py:13:22:13:28 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_mail.py:13:22:13:33 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| flask_mail.py:13:22:13:41 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| flask_mail.py:18:14:18:20 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_mail.py:18:14:18:25 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| flask_mail.py:18:14:18:33 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| flask_mail.py:31:24:31:30 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_mail.py:31:24:31:35 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| flask_mail.py:31:24:31:43 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| sendgrid_mail.py:14:22:14:28 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| sendgrid_mail.py:14:22:14:33 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| sendgrid_mail.py:14:22:14:49 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| sendgrid_mail.py:26:22:26:62 | ControlFlowNode for HtmlContent() | semmle.label | ControlFlowNode for HtmlContent() |
|
||||
| sendgrid_mail.py:26:34:26:40 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| sendgrid_mail.py:26:34:26:45 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| sendgrid_mail.py:26:34:26:61 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| sendgrid_mail.py:37:41:37:47 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| sendgrid_mail.py:37:41:37:52 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| sendgrid_mail.py:37:41:37:68 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:16:26:16:79 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:62 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:78 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:27:25:27:77 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:56 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:61 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:76 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:56 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:61 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:78 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| smtplib_bad_subparts.py:17:12:17:18 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| smtplib_bad_subparts.py:17:12:17:23 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| smtplib_bad_subparts.py:17:12:17:33 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| smtplib_bad_subparts.py:24:22:24:25 | ControlFlowNode for html | semmle.label | ControlFlowNode for html |
|
||||
| smtplib_bad_via_attach.py:20:12:20:18 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| smtplib_bad_via_attach.py:20:12:20:23 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| smtplib_bad_via_attach.py:20:12:20:31 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| smtplib_bad_via_attach.py:27:22:27:25 | ControlFlowNode for html | semmle.label | ControlFlowNode for html |
|
||||
subpaths
|
||||
#select
|
||||
| django_mail.py:14:48:14:82 | ControlFlowNode for Attribute() | django_mail.py:14:48:14:82 | ControlFlowNode for Attribute() | django_mail.py:14:48:14:82 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | django_mail.py:14:48:14:82 | ControlFlowNode for Attribute() | a user-provided value |
|
||||
| django_mail.py:23:30:23:64 | ControlFlowNode for Attribute() | django_mail.py:23:30:23:64 | ControlFlowNode for Attribute() | django_mail.py:23:30:23:64 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | django_mail.py:23:30:23:64 | ControlFlowNode for Attribute() | a user-provided value |
|
||||
| django_mail.py:25:32:25:66 | ControlFlowNode for Attribute() | django_mail.py:25:32:25:66 | ControlFlowNode for Attribute() | django_mail.py:25:32:25:66 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | django_mail.py:25:32:25:66 | ControlFlowNode for Attribute() | a user-provided value |
|
||||
| flask_mail.py:13:22:13:41 | ControlFlowNode for Subscript | flask_mail.py:13:22:13:28 | ControlFlowNode for request | flask_mail.py:13:22:13:41 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:13:22:13:28 | ControlFlowNode for request | a user-provided value |
|
||||
| flask_mail.py:18:14:18:33 | ControlFlowNode for Subscript | flask_mail.py:13:22:13:28 | ControlFlowNode for request | flask_mail.py:18:14:18:33 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:13:22:13:28 | ControlFlowNode for request | a user-provided value |
|
||||
| flask_mail.py:18:14:18:33 | ControlFlowNode for Subscript | flask_mail.py:18:14:18:20 | ControlFlowNode for request | flask_mail.py:18:14:18:33 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:18:14:18:20 | ControlFlowNode for request | a user-provided value |
|
||||
| flask_mail.py:31:24:31:43 | ControlFlowNode for Subscript | flask_mail.py:31:24:31:30 | ControlFlowNode for request | flask_mail.py:31:24:31:43 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:31:24:31:30 | ControlFlowNode for request | a user-provided value |
|
||||
| sendgrid_mail.py:14:22:14:49 | ControlFlowNode for Subscript | sendgrid_mail.py:14:22:14:28 | ControlFlowNode for request | sendgrid_mail.py:14:22:14:49 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | sendgrid_mail.py:14:22:14:28 | ControlFlowNode for request | a user-provided value |
|
||||
| sendgrid_mail.py:26:22:26:62 | ControlFlowNode for HtmlContent() | sendgrid_mail.py:26:34:26:40 | ControlFlowNode for request | sendgrid_mail.py:26:22:26:62 | ControlFlowNode for HtmlContent() | Cross-site scripting vulnerability due to $@. | sendgrid_mail.py:26:34:26:40 | ControlFlowNode for request | a user-provided value |
|
||||
| sendgrid_mail.py:37:41:37:68 | ControlFlowNode for Subscript | sendgrid_mail.py:37:41:37:47 | ControlFlowNode for request | sendgrid_mail.py:37:41:37:68 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | sendgrid_mail.py:37:41:37:47 | ControlFlowNode for request | a user-provided value |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:16:26:16:79 | ControlFlowNode for Attribute() | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:16:26:16:79 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | a user-provided value |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:27:25:27:77 | ControlFlowNode for Attribute() | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:27:25:27:77 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | a user-provided value |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:27:25:27:77 | ControlFlowNode for Attribute() | sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:56 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:27:25:27:77 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:56 | ControlFlowNode for request | a user-provided value |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | a user-provided value |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:56 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:56 | ControlFlowNode for request | a user-provided value |
|
||||
| sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:56 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:56 | ControlFlowNode for request | a user-provided value |
|
||||
| smtplib_bad_subparts.py:24:22:24:25 | ControlFlowNode for html | smtplib_bad_subparts.py:17:12:17:18 | ControlFlowNode for request | smtplib_bad_subparts.py:24:22:24:25 | ControlFlowNode for html | Cross-site scripting vulnerability due to $@. | smtplib_bad_subparts.py:17:12:17:18 | ControlFlowNode for request | a user-provided value |
|
||||
| smtplib_bad_via_attach.py:27:22:27:25 | ControlFlowNode for html | smtplib_bad_via_attach.py:20:12:20:18 | ControlFlowNode for request | smtplib_bad_via_attach.py:27:22:27:25 | ControlFlowNode for html | Cross-site scripting vulnerability due to $@. | smtplib_bad_via_attach.py:20:12:20:18 | ControlFlowNode for request | a user-provided value |
|
||||
@@ -0,0 +1 @@
|
||||
experimental/Security/CWE-079/ReflectedXSS.ql
|
||||
@@ -0,0 +1,25 @@
|
||||
import django.http
|
||||
from django.core.mail import send_mail, mail_admins, mail_managers
|
||||
|
||||
|
||||
def django_response(request):
|
||||
"""
|
||||
The Django.core.mail#send_mail function source code can be found in the link below:
|
||||
https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/core/mail/__init__.py#L38
|
||||
|
||||
send_mass_mail does not provide html_message as an argument to it's function. See the link below for more info:
|
||||
https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/core/mail/__init__.py#L64
|
||||
"""
|
||||
send_mail("Subject", "plain-text body", "from@example.com",
|
||||
["to@example.com"], html_message=django.http.request.GET.get("html"))
|
||||
|
||||
|
||||
def django_response(request):
|
||||
"""
|
||||
The Django.core.mail#mail_admins and Django.core.mail#mail_managers functions source code can be found in the link below:
|
||||
https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/core/mail/__init__.py#L90-L121
|
||||
"""
|
||||
mail_admins("Subject", "plain-text body",
|
||||
html_message=django.http.request.GET.get("html"))
|
||||
mail_managers("Subject", "plain-text body",
|
||||
html_message=django.http.request.GET.get("html"))
|
||||
@@ -0,0 +1,32 @@
|
||||
from flask import request, Flask
|
||||
from flask_mail import Mail, Message
|
||||
|
||||
app = Flask(__name__)
|
||||
mail = Mail(app)
|
||||
|
||||
@app.route("/send")
|
||||
def send():
|
||||
msg = Message(subject="Subject",
|
||||
sender="from@example.com",
|
||||
recipients=["to@example.com"],
|
||||
body="plain-text body",
|
||||
html=request.args["html"])
|
||||
|
||||
# The message can contain a body and/or HTML:
|
||||
msg.body = "plain-text body"
|
||||
# The email's HTML can be set via msg.html or as an initialize argument when creating a Message object.
|
||||
msg.html = request.args["html"]
|
||||
|
||||
mail.send(msg)
|
||||
|
||||
@app.route("/connect")
|
||||
def connect():
|
||||
"""
|
||||
Minimal example to test mail.connect() usage
|
||||
"""
|
||||
with mail.connect() as conn:
|
||||
msg = Message(subject="Subject",
|
||||
sender="from@example.com",
|
||||
recipients=["to@example.com"],
|
||||
html=request.args["html"])
|
||||
conn.send(msg)
|
||||
@@ -0,0 +1,57 @@
|
||||
from flask import request, Flask
|
||||
from sendgrid import SendGridAPIClient
|
||||
from sendgrid.helpers.mail import Mail, Email, To, Content, MimeType, HtmlContent
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/send")
|
||||
def send():
|
||||
message = Mail(
|
||||
from_email='from_email@example.com',
|
||||
to_emails='to@example.com',
|
||||
subject='Sending with Twilio SendGrid is Fun',
|
||||
html_content=request.args["html_content"])
|
||||
|
||||
sg = SendGridAPIClient('SENDGRID_API_KEY')
|
||||
sg.send(message)
|
||||
|
||||
|
||||
@app.route("/send-HtmlContent")
|
||||
def send():
|
||||
message = Mail(
|
||||
from_email='from_email@example.com',
|
||||
to_emails='to@example.com',
|
||||
subject='Sending with Twilio SendGrid is Fun',
|
||||
html_content=HtmlContent(request.args["html_content"]))
|
||||
|
||||
sg = SendGridAPIClient('SENDGRID_API_KEY')
|
||||
sg.send(message)
|
||||
|
||||
|
||||
@app.route("/send_post")
|
||||
def send_post():
|
||||
from_email = Email("test@example.com")
|
||||
to_email = To("test@example.com")
|
||||
subject = "Sending with SendGrid is Fun"
|
||||
html_content = Content("text/html", request.args["html_content"])
|
||||
plain_content = Content("text/plain", request.args["plain_content"])
|
||||
|
||||
mail = Mail(from_email, to_email, subject, plain_content, html_content)
|
||||
|
||||
sg = SendGridAPIClient(api_key='SENDGRID_API_KEY')
|
||||
response = sg.client.mail.send.post(request_body=mail.get())
|
||||
|
||||
|
||||
@app.route("/send_post2")
|
||||
def send_post2():
|
||||
from_email = Email("test@example.com")
|
||||
to_email = To("test@example.com")
|
||||
subject = "Sending with SendGrid is Fun"
|
||||
html_content = Content(MimeType.html, request.args["html_content"])
|
||||
plain_content = Content(MimeType.text, request.args["plain_content"])
|
||||
|
||||
mail = Mail(from_email, to_email, subject, plain_content, html_content)
|
||||
|
||||
sg = SendGridAPIClient(api_key='SENDGRID_API_KEY')
|
||||
response = sg.client.mail.send.post(request_body=mail.get())
|
||||
@@ -0,0 +1,48 @@
|
||||
import sendgrid
|
||||
import os
|
||||
from flask import request, Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/sendgrid")
|
||||
def send():
|
||||
sg = sendgrid.SendGridAPIClient(os.environ.get('SENDGRID_API_KEY'))
|
||||
|
||||
data = {
|
||||
"content": [
|
||||
{
|
||||
"type": "text/html",
|
||||
"value": "<html>{}</html>".format(request.args["html_content"])
|
||||
}
|
||||
],
|
||||
"from": {
|
||||
"email": "sam.smith@example.com",
|
||||
"name": "Sam Smith"
|
||||
},
|
||||
"headers": {},
|
||||
"mail_settings": {
|
||||
"footer": {
|
||||
"enable": True,
|
||||
"html": "<html>{}</html>".format(request.args["html_footer"]),
|
||||
"text": "Thanks,/n The SendGrid Team"
|
||||
},
|
||||
},
|
||||
"reply_to": {
|
||||
"email": "sam.smith@example.com",
|
||||
"name": "Sam Smith"
|
||||
},
|
||||
"send_at": 1409348513,
|
||||
"subject": "Hello, World!",
|
||||
"template_id": "[YOUR TEMPLATE ID GOES HERE]",
|
||||
"tracking_settings": {
|
||||
"subscription_tracking": {
|
||||
"enable": True,
|
||||
"html": "<html>{}</html>".format(request.args["html_tracking"]),
|
||||
"substitution_tag": "<%click here%>",
|
||||
"text": "If you would like to unsubscribe and stop receiving these emails <% click here %>."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response = sg.client.mail.send.post(request_body=data)
|
||||
@@ -0,0 +1,42 @@
|
||||
# This test checks that the developer doesn't pass a MIMEText instance to a MIMEMultipart initializer via the subparts parameter.
|
||||
from flask import Flask, request
|
||||
import json
|
||||
import smtplib
|
||||
import ssl
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def email_person():
|
||||
sender_email = "sender@gmail.com"
|
||||
receiver_email = "receiver@example.com"
|
||||
|
||||
name = request.args['search']
|
||||
# Create the plain-text and HTML version of your message
|
||||
text = "hello there"
|
||||
html = f"hello {name}"
|
||||
|
||||
# Turn these into plain/html MIMEText objects
|
||||
part1 = MIMEText(text, "plain")
|
||||
part2 = MIMEText(html, "html")
|
||||
|
||||
message = MIMEMultipart(_subparts=(part1, part2))
|
||||
message["Subject"] = "multipart test"
|
||||
message["From"] = sender_email
|
||||
message["To"] = receiver_email
|
||||
|
||||
# Create secure connection with server and send email
|
||||
context = ssl.create_default_context()
|
||||
server = smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context)
|
||||
|
||||
server.login(sender_email, "SERVER_PASSWORD")
|
||||
server.sendmail(
|
||||
sender_email, receiver_email, message.as_string()
|
||||
)
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# app.run(debug=True)
|
||||
@@ -0,0 +1,45 @@
|
||||
# This test checks that the developer doesn't pass a MIMEText instance to a MIMEMultipart message.
|
||||
from flask import Flask, request
|
||||
import json
|
||||
import smtplib, ssl
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/")
|
||||
def email_person():
|
||||
sender_email = "sender@gmail.com"
|
||||
receiver_email = "receiver@example.com"
|
||||
|
||||
message = MIMEMultipart("alternative")
|
||||
message["Subject"] = "multipart test"
|
||||
message["From"] = sender_email
|
||||
message["To"] = receiver_email
|
||||
|
||||
name = request.args['name']
|
||||
# Create the plain-text and HTML version of your message
|
||||
text = "hello there"
|
||||
html = f"hello {name}"
|
||||
|
||||
# Turn these into plain/html MIMEText objects
|
||||
part1 = MIMEText(text, "plain")
|
||||
part2 = MIMEText(html, "html")
|
||||
|
||||
# Add HTML/plain-text parts to MIMEMultipart message
|
||||
# The email client will try to render the last part first
|
||||
message.attach(part1)
|
||||
message.attach(part2)
|
||||
|
||||
# Create secure connection with server and send email
|
||||
context = ssl.create_default_context()
|
||||
server = smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context)
|
||||
|
||||
server.login(sender_email, "SERVER_PASSWORD")
|
||||
server.sendmail(
|
||||
sender_email, receiver_email, message.as_string()
|
||||
)
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# app.run(debug=True)
|
||||
@@ -0,0 +1,19 @@
|
||||
edges
|
||||
| csv_bad.py:16:16:16:22 | ControlFlowNode for request | csv_bad.py:16:16:16:27 | ControlFlowNode for Attribute |
|
||||
| csv_bad.py:16:16:16:27 | ControlFlowNode for Attribute | csv_bad.py:18:24:18:31 | ControlFlowNode for csv_data |
|
||||
| csv_bad.py:16:16:16:27 | ControlFlowNode for Attribute | csv_bad.py:19:25:19:32 | ControlFlowNode for csv_data |
|
||||
| csv_bad.py:24:16:24:22 | ControlFlowNode for request | csv_bad.py:24:16:24:27 | ControlFlowNode for Attribute |
|
||||
| csv_bad.py:24:16:24:27 | ControlFlowNode for Attribute | csv_bad.py:25:46:25:53 | ControlFlowNode for csv_data |
|
||||
nodes
|
||||
| csv_bad.py:16:16:16:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| csv_bad.py:16:16:16:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| csv_bad.py:18:24:18:31 | ControlFlowNode for csv_data | semmle.label | ControlFlowNode for csv_data |
|
||||
| csv_bad.py:19:25:19:32 | ControlFlowNode for csv_data | semmle.label | ControlFlowNode for csv_data |
|
||||
| csv_bad.py:24:16:24:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| csv_bad.py:24:16:24:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| csv_bad.py:25:46:25:53 | ControlFlowNode for csv_data | semmle.label | ControlFlowNode for csv_data |
|
||||
subpaths
|
||||
#select
|
||||
| csv_bad.py:18:24:18:31 | ControlFlowNode for csv_data | csv_bad.py:16:16:16:22 | ControlFlowNode for request | csv_bad.py:18:24:18:31 | ControlFlowNode for csv_data | Csv injection might include code from $@. | csv_bad.py:16:16:16:22 | ControlFlowNode for request | this user input |
|
||||
| csv_bad.py:19:25:19:32 | ControlFlowNode for csv_data | csv_bad.py:16:16:16:22 | ControlFlowNode for request | csv_bad.py:19:25:19:32 | ControlFlowNode for csv_data | Csv injection might include code from $@. | csv_bad.py:16:16:16:22 | ControlFlowNode for request | this user input |
|
||||
| csv_bad.py:25:46:25:53 | ControlFlowNode for csv_data | csv_bad.py:24:16:24:22 | ControlFlowNode for request | csv_bad.py:25:46:25:53 | ControlFlowNode for csv_data | Csv injection might include code from $@. | csv_bad.py:24:16:24:22 | ControlFlowNode for request | this user input |
|
||||
@@ -0,0 +1 @@
|
||||
experimental/Security/CWE-1236/CsvInjection.ql
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
"""
|
||||
@Desc :csv injection
|
||||
"""
|
||||
import copy
|
||||
import csv
|
||||
from flask import Flask
|
||||
from flask import request
|
||||
from typing import List
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/bad1')
|
||||
def bad1():
|
||||
csv_data = request.args.get('csv')
|
||||
csvWriter = csv.writer(open("test.csv", "wt"))
|
||||
csvWriter.writerow(csv_data) # bad
|
||||
csvWriter.writerows(csv_data) # bad
|
||||
return "bad1"
|
||||
|
||||
@app.route('/bad2')
|
||||
def bad2():
|
||||
csv_data = request.args.get('csv')
|
||||
csvWriter = csv.DictWriter(f, fieldnames=csv_data) # bad
|
||||
csvWriter.writeheader()
|
||||
return "bad2"
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.debug = True
|
||||
app.run()
|
||||
@@ -1,6 +1,13 @@
|
||||
| test.py:4:1:4:23 | Class MyTest |
|
||||
| test.py:6:5:6:21 | Function test_1 |
|
||||
| test.py:9:5:9:21 | Function test_2 |
|
||||
| test_foo.py:3:1:3:15 | Function test_foo |
|
||||
| unittest_test.py:3:1:3:33 | Class FooTest |
|
||||
| unittest_test.py:4:5:4:25 | Function test_valid |
|
||||
| unittest_test.py:7:1:7:49 | Class FunctionFooTest |
|
||||
| unittest_test.py:8:5:8:25 | Function test_valid |
|
||||
| unittest_test.py:11:1:11:50 | Class AsyncTest |
|
||||
| unittest_test.py:12:11:12:31 | Function test_valid |
|
||||
| unittest_test.py:17:1:17:45 | Class MyDjangoUnitTest |
|
||||
| unittest_test.py:18:5:18:25 | Function test_valid |
|
||||
| unittest_test.py:23:1:23:56 | Class MyFlaskUnitTest |
|
||||
| unittest_test.py:24:5:24:25 | Function test_valid |
|
||||
| unittest_test.py:29:1:29:59 | Class MyTornadoUnitTest |
|
||||
| unittest_test.py:30:5:30:25 | Function test_valid |
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
class TestCase:
|
||||
pass
|
||||
|
||||
class MyTest(TestCase):
|
||||
|
||||
def test_1(self):
|
||||
pass
|
||||
|
||||
def test_2(self):
|
||||
pass
|
||||
@@ -3,3 +3,29 @@ import unittest
|
||||
class FooTest(unittest.TestCase):
|
||||
def test_valid(self):
|
||||
pass
|
||||
|
||||
class FunctionFooTest(unittest.FunctionTestCase):
|
||||
def test_valid(self):
|
||||
pass
|
||||
|
||||
class AsyncTest(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_valid(self):
|
||||
pass
|
||||
|
||||
# django -- see https://docs.djangoproject.com/en/4.0/topics/testing/overview/
|
||||
import django.test
|
||||
class MyDjangoUnitTest(django.test.TestCase):
|
||||
def test_valid(self):
|
||||
pass
|
||||
|
||||
# flask -- see https://pythonhosted.org/Flask-Testing/
|
||||
import flask_testing
|
||||
class MyFlaskUnitTest(flask_testing.LiveServerTestCase):
|
||||
def test_valid(self):
|
||||
pass
|
||||
|
||||
# tornado -- see https://www.tornadoweb.org/en/stable/testing.html#tornado.testing.AsyncHTTPTestCase
|
||||
import tornado.testing
|
||||
class MyTornadoUnitTest(tornado.testing.AsyncHTTPTestCase):
|
||||
def test_valid(self):
|
||||
pass
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import ssl
|
||||
|
||||
s = aiohttp.ClientSession()
|
||||
resp = s.request("method", "url") # $ clientRequestUrlPart="url"
|
||||
@@ -13,4 +14,22 @@ with aiohttp.ClientSession() as session:
|
||||
s = aiohttp.ClientSession()
|
||||
resp = s.post("url") # $ clientRequestUrlPart="url"
|
||||
resp = s.patch("url") # $ clientRequestUrlPart="url"
|
||||
resp = s.options("url") # $ clientRequestUrlPart="url"
|
||||
resp = s.options("url") # $ clientRequestUrlPart="url"
|
||||
|
||||
# disabling of SSL validation
|
||||
# see https://docs.aiohttp.org/en/stable/client_reference.html#aiohttp.ClientSession.request
|
||||
s.get("url", ssl=False) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
s.get("url", ssl=0) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
# None is treated as default and so does _not_ disable the check
|
||||
s.get("url", ssl=None) # $ clientRequestUrlPart="url"
|
||||
|
||||
# deprecated since 3.0, but still supported
|
||||
s.get("url", verify_ssl=False) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
|
||||
# A manually constructed SSLContext does not have safe defaults, so is effectively the
|
||||
# same as turning off SSL validation
|
||||
context = ssl.SSLContext()
|
||||
assert context.check_hostname == False
|
||||
assert context.verify_mode == ssl.VerifyMode.CERT_NONE
|
||||
|
||||
s.get("url", ssl=context) # $ clientRequestUrlPart="url" MISSING: clientRequestCertValidationDisabled
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import python
|
||||
import experimental.meta.MaDTest
|
||||
@@ -7,17 +7,17 @@ async def test_connection():
|
||||
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_query("sql", output="filepath") # $ mad-sink[sql-injection]="sql" mad-sink[path-injection]="filepath"
|
||||
await conn.copy_from_query("sql", "arg1", "arg2", output="filepath") # $ mad-sink[sql-injection]="sql" mad-sink[path-injection]="filepath"
|
||||
|
||||
await conn.copy_from_table("table", output="filepath") # $ getAPathArgument="filepath"
|
||||
await conn.copy_to_table("table", source="filepath") # $ getAPathArgument="filepath"
|
||||
await conn.copy_from_table("table", output="filepath") # $ mad-sink[path-injection]="filepath"
|
||||
await conn.copy_to_table("table", source="filepath") # $ mad-sink[path-injection]="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"
|
||||
await conn.execute("sql") # $ mad-sink[sql-injection]="sql"
|
||||
await conn.executemany("sql") # $ mad-sink[sql-injection]="sql"
|
||||
await conn.fetch("sql") # $ mad-sink[sql-injection]="sql"
|
||||
await conn.fetchrow("sql") # $ mad-sink[sql-injection]="sql"
|
||||
await conn.fetchval("sql") # $ mad-sink[sql-injection]="sql"
|
||||
|
||||
finally:
|
||||
await conn.close()
|
||||
@@ -27,11 +27,11 @@ 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"
|
||||
pstmt = await conn.prepare("psql") # $ mad-sink[sql-injection]="psql"
|
||||
pstmt.executemany()
|
||||
pstmt.fetch()
|
||||
pstmt.fetchrow()
|
||||
pstmt.fetchval()
|
||||
|
||||
finally:
|
||||
await conn.close()
|
||||
@@ -46,7 +46,7 @@ async def test_cursor():
|
||||
cursor = await conn.cursor("sql") # $ getSql="sql" constructedSql="sql"
|
||||
await cursor.fetch()
|
||||
|
||||
pstmt = await conn.prepare("psql") # $ constructedSql="psql"
|
||||
pstmt = await conn.prepare("psql") # $ mad-sink[sql-injection]="psql"
|
||||
pcursor = await pstmt.cursor() # $ getSql="psql"
|
||||
await pcursor.fetch()
|
||||
|
||||
@@ -69,23 +69,23 @@ 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.copy_from_query("sql", output="filepath") # $ mad-sink[sql-injection]="sql" mad-sink[path-injection]="filepath"
|
||||
await pool.copy_from_query("sql", "arg1", "arg2", output="filepath") # $ mad-sink[sql-injection]="sql" mad-sink[path-injection]="filepath"
|
||||
await pool.copy_from_table("table", output="filepath") # $ mad-sink[path-injection]="filepath"
|
||||
await pool.copy_to_table("table", source="filepath") # $ mad-sink[path-injection]="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"
|
||||
await pool.execute("sql") # $ mad-sink[sql-injection]="sql"
|
||||
await pool.executemany("sql") # $ mad-sink[sql-injection]="sql"
|
||||
await pool.fetch("sql") # $ mad-sink[sql-injection]="sql"
|
||||
await pool.fetchrow("sql") # $ mad-sink[sql-injection]="sql"
|
||||
await pool.fetchval("sql") # $ mad-sink[sql-injection]="sql"
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute("sql") # $ getSql="sql"
|
||||
await conn.execute("sql") # $ mad-sink[sql-injection]="sql"
|
||||
|
||||
conn = await pool.acquire()
|
||||
try:
|
||||
await conn.fetch("sql") # $ getSql="sql"
|
||||
await conn.fetch("sql") # $ mad-sink[sql-injection]="sql"
|
||||
finally:
|
||||
await pool.release(conn)
|
||||
|
||||
@@ -93,13 +93,13 @@ async def test_connection_pool():
|
||||
await pool.close()
|
||||
|
||||
async with asyncpg.create_pool() as pool:
|
||||
await pool.execute("sql") # $ getSql="sql"
|
||||
await pool.execute("sql") # $ mad-sink[sql-injection]="sql"
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute("sql") # $ getSql="sql"
|
||||
await conn.execute("sql") # $ mad-sink[sql-injection]="sql"
|
||||
|
||||
conn = await pool.acquire()
|
||||
try:
|
||||
await conn.fetch("sql") # $ getSql="sql"
|
||||
await conn.fetch("sql") # $ mad-sink[sql-injection]="sql"
|
||||
finally:
|
||||
await pool.release(conn)
|
||||
|
||||
@@ -21,14 +21,14 @@ padding = b"\0"*padding_len
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
|
||||
# using separate .encrypt calls on individual lines does not work
|
||||
whole_plantext = secret_message + padding
|
||||
encrypted = cipher.encrypt(whole_plantext) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=whole_plantext
|
||||
encrypted = cipher.encrypt(whole_plantext) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=whole_plantext CryptographicOperationBlockMode=CBC
|
||||
|
||||
print("encrypted={}".format(encrypted))
|
||||
|
||||
print()
|
||||
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
|
||||
decrypted = cipher.decrypt(encrypted) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=encrypted
|
||||
decrypted = cipher.decrypt(encrypted) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=encrypted CryptographicOperationBlockMode=CBC
|
||||
|
||||
decrypted = decrypted[:-padding_len]
|
||||
|
||||
|
||||
@@ -21,14 +21,14 @@ padding = b"\0"*padding_len
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
|
||||
# using separate .encrypt calls on individual lines does not work
|
||||
whole_plantext = secret_message + padding
|
||||
encrypted = cipher.encrypt(whole_plantext) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=whole_plantext
|
||||
encrypted = cipher.encrypt(whole_plantext) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=whole_plantext CryptographicOperationBlockMode=CBC
|
||||
|
||||
print("encrypted={}".format(encrypted))
|
||||
|
||||
print()
|
||||
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
|
||||
decrypted = cipher.decrypt(encrypted) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=encrypted
|
||||
decrypted = cipher.decrypt(encrypted) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=encrypted CryptographicOperationBlockMode=CBC
|
||||
|
||||
decrypted = decrypted[:-padding_len]
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ padding = b"\0"*padding_len
|
||||
|
||||
encryptor = cipher.encryptor()
|
||||
print(padding_len)
|
||||
encrypted = encryptor.update(secret_message) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=secret_message
|
||||
encrypted += encryptor.update(padding) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=padding
|
||||
encrypted = encryptor.update(secret_message) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=secret_message CryptographicOperationBlockMode=CBC
|
||||
encrypted += encryptor.update(padding) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=padding CryptographicOperationBlockMode=CBC
|
||||
encrypted += encryptor.finalize()
|
||||
|
||||
print("encrypted={}".format(encrypted))
|
||||
@@ -31,7 +31,7 @@ print("encrypted={}".format(encrypted))
|
||||
print()
|
||||
|
||||
decryptor = cipher.decryptor()
|
||||
decrypted = decryptor.update(encrypted) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=encrypted
|
||||
decrypted = decryptor.update(encrypted) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=encrypted CryptographicOperationBlockMode=CBC
|
||||
decrypted += decryptor.finalize()
|
||||
|
||||
decrypted = decrypted[:-padding_len]
|
||||
|
||||
103
python/ql/test/library-tests/frameworks/data/test.expected
Normal file
103
python/ql/test/library-tests/frameworks/data/test.expected
Normal file
@@ -0,0 +1,103 @@
|
||||
taintFlow
|
||||
| test.py:3:5:3:15 | ControlFlowNode for getSource() | test.py:4:8:4:8 | ControlFlowNode for x |
|
||||
| test.py:3:5:3:15 | ControlFlowNode for getSource() | test.py:7:17:7:17 | ControlFlowNode for x |
|
||||
| test.py:9:8:9:14 | ControlFlowNode for alias() | test.py:9:8:9:14 | ControlFlowNode for alias() |
|
||||
| test.py:10:8:10:22 | ControlFlowNode for Attribute() | test.py:10:8:10:22 | ControlFlowNode for Attribute() |
|
||||
| test.py:11:8:11:30 | ControlFlowNode for Attribute() | test.py:11:8:11:30 | ControlFlowNode for Attribute() |
|
||||
| test.py:71:28:71:38 | ControlFlowNode for getSource() | test.py:71:8:71:39 | ControlFlowNode for Attribute() |
|
||||
| test.py:75:5:75:15 | ControlFlowNode for getSource() | test.py:76:22:76:22 | ControlFlowNode for x |
|
||||
| test.py:75:5:75:15 | ControlFlowNode for getSource() | test.py:77:22:77:22 | ControlFlowNode for y |
|
||||
| test.py:81:36:81:46 | ControlFlowNode for getSource() | test.py:81:8:81:47 | ControlFlowNode for Attribute() |
|
||||
| test.py:83:50:83:60 | ControlFlowNode for getSource() | test.py:83:8:83:61 | ControlFlowNode for Attribute() |
|
||||
| test.py:86:49:86:59 | ControlFlowNode for getSource() | test.py:86:8:86:60 | ControlFlowNode for Attribute() |
|
||||
| test.py:87:56:87:66 | ControlFlowNode for getSource() | test.py:87:8:87:67 | ControlFlowNode for Attribute() |
|
||||
isSink
|
||||
| test.py:4:8:4:8 | ControlFlowNode for x | test-sink |
|
||||
| test.py:7:17:7:17 | ControlFlowNode for x | test-sink |
|
||||
| test.py:9:8:9:14 | ControlFlowNode for alias() | test-sink |
|
||||
| test.py:10:8:10:22 | ControlFlowNode for Attribute() | test-sink |
|
||||
| test.py:11:8:11:30 | ControlFlowNode for Attribute() | test-sink |
|
||||
| test.py:12:8:12:34 | ControlFlowNode for Attribute() | test-sink |
|
||||
| test.py:16:11:16:13 | ControlFlowNode for one | test-sink |
|
||||
| test.py:17:19:17:21 | ControlFlowNode for two | test-sink |
|
||||
| test.py:17:24:17:28 | ControlFlowNode for three | test-sink |
|
||||
| test.py:17:31:17:34 | ControlFlowNode for four | test-sink |
|
||||
| test.py:18:37:18:40 | ControlFlowNode for five | test-sink |
|
||||
| test.py:19:21:19:26 | ControlFlowNode for second | test-sink |
|
||||
| test.py:30:21:30:23 | ControlFlowNode for one | test-sink |
|
||||
| test.py:32:22:32:24 | ControlFlowNode for one | test-sink |
|
||||
| test.py:32:27:32:29 | ControlFlowNode for two | test-sink |
|
||||
| test.py:33:22:33:24 | ControlFlowNode for one | test-sink |
|
||||
| test.py:33:27:33:29 | ControlFlowNode for two | test-sink |
|
||||
| test.py:33:32:33:36 | ControlFlowNode for three | test-sink |
|
||||
| test.py:57:27:57:33 | ControlFlowNode for arg_pos | test-sink |
|
||||
| test.py:66:17:66:20 | ControlFlowNode for arg1 | test-sink |
|
||||
| test.py:66:23:66:26 | ControlFlowNode for arg2 | test-sink |
|
||||
| test.py:66:34:66:43 | ControlFlowNode for namedThing | test-sink |
|
||||
| test.py:67:34:67:44 | ControlFlowNode for secondNamed | test-sink |
|
||||
| test.py:71:8:71:39 | ControlFlowNode for Attribute() | test-sink |
|
||||
| test.py:72:8:72:47 | ControlFlowNode for Attribute() | test-sink |
|
||||
| test.py:76:22:76:22 | ControlFlowNode for x | test-sink |
|
||||
| test.py:77:22:77:22 | ControlFlowNode for y | test-sink |
|
||||
| test.py:78:22:78:22 | ControlFlowNode for z | test-sink |
|
||||
| test.py:81:8:81:47 | ControlFlowNode for Attribute() | test-sink |
|
||||
| test.py:82:8:82:54 | ControlFlowNode for Attribute() | test-sink |
|
||||
| test.py:83:8:83:61 | ControlFlowNode for Attribute() | test-sink |
|
||||
| test.py:85:8:85:53 | ControlFlowNode for Attribute() | test-sink |
|
||||
| test.py:86:8:86:60 | ControlFlowNode for Attribute() | test-sink |
|
||||
| test.py:87:8:87:67 | ControlFlowNode for Attribute() | test-sink |
|
||||
| test.py:89:21:89:23 | ControlFlowNode for one | test-sink |
|
||||
| test.py:91:21:91:23 | ControlFlowNode for one | test-sink |
|
||||
| test.py:91:30:91:32 | ControlFlowNode for two | test-sink |
|
||||
| test.py:98:6:98:9 | ControlFlowNode for baz2 | test-sink |
|
||||
isSource
|
||||
| test.py:3:5:3:15 | ControlFlowNode for getSource() | test-source |
|
||||
| test.py:9:8:9:14 | ControlFlowNode for alias() | test-source |
|
||||
| test.py:10:8:10:14 | ControlFlowNode for alias() | test-source |
|
||||
| test.py:10:8:10:22 | ControlFlowNode for Attribute() | test-source |
|
||||
| test.py:11:8:11:14 | ControlFlowNode for alias() | test-source |
|
||||
| test.py:11:8:11:22 | ControlFlowNode for Attribute() | test-source |
|
||||
| test.py:11:8:11:30 | ControlFlowNode for Attribute() | test-source |
|
||||
| test.py:12:8:12:14 | ControlFlowNode for alias() | test-source |
|
||||
| test.py:12:8:12:22 | ControlFlowNode for Attribute() | test-source |
|
||||
| test.py:23:24:23:26 | ControlFlowNode for one | test-source |
|
||||
| test.py:24:33:24:35 | ControlFlowNode for two | test-source |
|
||||
| test.py:24:38:24:42 | ControlFlowNode for three | test-source |
|
||||
| test.py:24:45:24:48 | ControlFlowNode for four | test-source |
|
||||
| test.py:25:34:25:39 | ControlFlowNode for second | test-source |
|
||||
| test.py:39:11:39:20 | ControlFlowNode for Await | test-source |
|
||||
| test.py:41:8:41:27 | ControlFlowNode for Attribute() | test-source |
|
||||
| test.py:46:7:46:16 | ControlFlowNode for SubClass() | test-source |
|
||||
| test.py:53:7:53:16 | ControlFlowNode for Attribute() | test-source |
|
||||
| test.py:60:13:60:16 | ControlFlowNode for self | test-source |
|
||||
| test.py:60:24:60:28 | ControlFlowNode for named | test-source |
|
||||
| test.py:63:36:63:39 | ControlFlowNode for arg2 | test-source |
|
||||
| test.py:63:42:63:45 | ControlFlowNode for arg3 | test-source |
|
||||
| test.py:63:48:63:51 | ControlFlowNode for arg4 | test-source |
|
||||
| test.py:63:54:63:57 | ControlFlowNode for arg5 | test-source |
|
||||
| test.py:71:28:71:38 | ControlFlowNode for getSource() | test-source |
|
||||
| test.py:72:36:72:46 | ControlFlowNode for getSource() | test-source |
|
||||
| test.py:75:5:75:15 | ControlFlowNode for getSource() | test-source |
|
||||
| test.py:81:36:81:46 | ControlFlowNode for getSource() | test-source |
|
||||
| test.py:82:43:82:53 | ControlFlowNode for getSource() | test-source |
|
||||
| test.py:83:50:83:60 | ControlFlowNode for getSource() | test-source |
|
||||
| test.py:85:42:85:52 | ControlFlowNode for getSource() | test-source |
|
||||
| test.py:86:49:86:59 | ControlFlowNode for getSource() | test-source |
|
||||
| test.py:87:56:87:66 | ControlFlowNode for getSource() | test-source |
|
||||
| test.py:101:29:101:31 | ControlFlowNode for arg | test-source |
|
||||
| test.py:104:24:104:29 | ControlFlowNode for param1 | test-source |
|
||||
| test.py:104:32:104:37 | ControlFlowNode for param2 | test-source |
|
||||
| test.py:107:24:107:28 | ControlFlowNode for name1 | test-source |
|
||||
| test.py:107:31:107:35 | ControlFlowNode for name2 | test-source |
|
||||
syntaxErrors
|
||||
| Member[foo |
|
||||
| Member[foo] .Member[bar] |
|
||||
| Member[foo] Member[bar] |
|
||||
| Member[foo], Member[bar] |
|
||||
| Member[foo],Member[bar] |
|
||||
| Member[foo]. Member[bar] |
|
||||
| Member[foo]..Member[bar] |
|
||||
| Member[foo]Member[bar] |
|
||||
| Member[foo]] |
|
||||
| Member[foo]].Member[bar] |
|
||||
warning
|
||||
108
python/ql/test/library-tests/frameworks/data/test.py
Normal file
108
python/ql/test/library-tests/frameworks/data/test.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from testlib import getSource, mySink, alias
|
||||
|
||||
x = getSource()
|
||||
mySink(x)
|
||||
|
||||
mySink(foo=x) # OK
|
||||
mySink(sinkName=x) # NOT OK
|
||||
|
||||
mySink(alias()) # NOT OK
|
||||
mySink(alias().chain()) # NOT OK
|
||||
mySink(alias().chain().chain()) # NOT OK
|
||||
mySink(alias().chain().safeThing()) # OK
|
||||
|
||||
from testlib import Args
|
||||
|
||||
Args.arg0(one, two, three, four, five)
|
||||
Args.arg1to3(one, two, three, four, five)
|
||||
Args.lastarg(one, two, three, four, five)
|
||||
Args.nonFist(first, second)
|
||||
|
||||
from testlib import Callbacks
|
||||
|
||||
Callbacks.first(lambda one, two, three, four, five: 0)
|
||||
Callbacks.param1to3(lambda one, two, three, four, five: 0)
|
||||
Callbacks.nonFirst(lambda first, second: 0)
|
||||
|
||||
from testlib import CallFilter
|
||||
|
||||
CallFilter.arityOne(one, two) # NO match
|
||||
CallFilter.arityOne(one) # Match
|
||||
CallFilter.twoOrMore(one) # NO match
|
||||
CallFilter.twoOrMore(one, two) # Match
|
||||
CallFilter.twoOrMore(one, two, three) # Match
|
||||
|
||||
from testlib import CommonTokens
|
||||
|
||||
async def async_func():
|
||||
prom = CommonTokens.makePromise(1);
|
||||
val = await prom
|
||||
|
||||
inst = CommonTokens.Class()
|
||||
|
||||
class SubClass (CommonTokens.Super):
|
||||
pass
|
||||
|
||||
sub = SubClass()
|
||||
|
||||
class Sub2Class (CommonTokens.Class):
|
||||
pass
|
||||
|
||||
sub2 = Sub2Class() # TODO: Currently not recognized as an instance of CommonTokens.Class
|
||||
|
||||
val = inst.foo()
|
||||
|
||||
from testlib import ArgPos
|
||||
|
||||
arg_pos = ArgPos(); val = arg_pos.self_thing(arg, named=2);
|
||||
|
||||
class SubClass (ArgPos.MyClass):
|
||||
def foo(self, arg, named=2, otherName=3):
|
||||
pass
|
||||
|
||||
def secondAndAfter(self, arg1, arg2, arg3, arg4, arg5):
|
||||
pass
|
||||
|
||||
ArgPos.anyParam(arg1, arg2, name=namedThing)
|
||||
ArgPos.anyNamed(arg4, arg5, name=secondNamed)
|
||||
|
||||
from testlib import Steps
|
||||
|
||||
mySink(Steps.preserveTaint(getSource())) # FLOW
|
||||
mySink(Steps.preserveTaint("safe", getSource())) # NO FLOW
|
||||
|
||||
Steps.taintIntoCallback(
|
||||
getSource(),
|
||||
lambda x: mySink(x), # FLOW
|
||||
lambda y: mySink(y), # FLOW
|
||||
lambda z: mySink(z) # NO FLOW
|
||||
)
|
||||
|
||||
mySink(Steps.preserveArgZeroAndTwo(getSource())) # FLOW
|
||||
mySink(Steps.preserveArgZeroAndTwo("foo", getSource())) # NO FLOW
|
||||
mySink(Steps.preserveArgZeroAndTwo("foo", "bar", getSource())) # FLOW
|
||||
|
||||
mySink(Steps.preserveAllButFirstArgument(getSource())) # NO FLOW
|
||||
mySink(Steps.preserveAllButFirstArgument("foo", getSource())) # FLOW
|
||||
mySink(Steps.preserveAllButFirstArgument("foo", "bar", getSource())) # FLOW
|
||||
|
||||
CallFilter.arityOne(one) # match
|
||||
CallFilter.arityOne(one=one) # NO match
|
||||
CallFilter.arityOne(one, two=two) # match - on both the named and positional arguments
|
||||
CallFilter.arityOne(one=one, two=two) # NO match
|
||||
|
||||
from foo1.bar import baz1
|
||||
baz1(baz1) # no match, and that's the point.
|
||||
|
||||
from foo2.bar import baz2
|
||||
baz2(baz2) # match
|
||||
|
||||
class OtherSubClass (ArgPos.MyClass):
|
||||
def otherSelfTest(self, arg, named=2, otherName=3): # test that Parameter[0] hits `arg`.
|
||||
pass
|
||||
|
||||
def anyParam(self, param1, param2): # Parameter[any] matches all non-self parameters
|
||||
pass
|
||||
|
||||
def anyNamed(self, name1, name2=2): # Parameter[any-named] matches all non-self named parameters
|
||||
pass
|
||||
127
python/ql/test/library-tests/frameworks/data/test.ql
Normal file
127
python/ql/test/library-tests/frameworks/data/test.ql
Normal file
@@ -0,0 +1,127 @@
|
||||
import python
|
||||
import semmle.python.frameworks.data.internal.AccessPathSyntax as AccessPathSyntax
|
||||
import semmle.python.frameworks.data.ModelsAsData
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
class Steps extends ModelInput::SummaryModelCsv {
|
||||
override predicate row(string row) {
|
||||
// package;type;path;input;output;kind
|
||||
row =
|
||||
[
|
||||
"testlib;;Member[Steps].Member[preserveTaint].Call;Argument[0];ReturnValue;taint",
|
||||
"testlib;;Member[Steps].Member[taintIntoCallback];Argument[0];Argument[1..2].Parameter[0];taint",
|
||||
"testlib;;Member[Steps].Member[preserveArgZeroAndTwo];Argument[0,2];ReturnValue;taint",
|
||||
"testlib;;Member[Steps].Member[preserveAllButFirstArgument].Call;Argument[1..];ReturnValue;taint",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class Types extends ModelInput::TypeModelCsv {
|
||||
override predicate row(string row) {
|
||||
// package1;type1;package2;type2;path
|
||||
row =
|
||||
[
|
||||
"testlib;Alias;testlib;;Member[alias].ReturnValue",
|
||||
"testlib;Alias;testlib;Alias;Member[chain].ReturnValue",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class Sinks extends ModelInput::SinkModelCsv {
|
||||
override predicate row(string row) {
|
||||
// package;type;path;kind
|
||||
row =
|
||||
[
|
||||
"testlib;;Member[mySink].Argument[0,sinkName:];test-sink",
|
||||
// testing argument syntax
|
||||
"testlib;;Member[Args].Member[arg0].Argument[0];test-sink", //
|
||||
"testlib;;Member[Args].Member[arg1to3].Argument[1..3];test-sink", //
|
||||
"testlib;;Member[Args].Member[lastarg].Argument[N-1];test-sink", //
|
||||
"testlib;;Member[Args].Member[nonFist].Argument[1..];test-sink", //
|
||||
// callsite filter.
|
||||
"testlib;;Member[CallFilter].Member[arityOne].WithArity[1].Argument[any];test-sink", //
|
||||
"testlib;;Member[CallFilter].Member[twoOrMore].WithArity[2..].Argument[0..];test-sink", //
|
||||
// testing non-positional arguments
|
||||
"testlib;;Member[ArgPos].Instance.Member[self_thing].Argument[self];test-sink", //
|
||||
// any argument
|
||||
"testlib;;Member[ArgPos].Member[anyParam].Argument[any];test-sink", //
|
||||
"testlib;;Member[ArgPos].Member[anyNamed].Argument[any-named];test-sink", //
|
||||
// testing package syntax
|
||||
"foo1.bar;;Member[baz1].Argument[any];test-sink", //
|
||||
"foo2;;Member[bar].Member[baz2].Argument[any];test-sink", //
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class Sources extends ModelInput::SourceModelCsv {
|
||||
// package;type;path;kind
|
||||
override predicate row(string row) {
|
||||
row =
|
||||
[
|
||||
"testlib;;Member[getSource].ReturnValue;test-source", //
|
||||
"testlib;Alias;;test-source",
|
||||
// testing parameter syntax
|
||||
"testlib;;Member[Callbacks].Member[first].Argument[0].Parameter[0];test-source", //
|
||||
"testlib;;Member[Callbacks].Member[param1to3].Argument[0].Parameter[1..3];test-source", //
|
||||
"testlib;;Member[Callbacks].Member[nonFirst].Argument[0].Parameter[1..];test-source", //
|
||||
// Common tokens.
|
||||
"testlib;;Member[CommonTokens].Member[makePromise].ReturnValue.Awaited;test-source", //
|
||||
"testlib;;Member[CommonTokens].Member[Class].Instance;test-source", //
|
||||
"testlib;;Member[CommonTokens].Member[Super].Subclass.Instance;test-source", //
|
||||
// method
|
||||
"testlib;;Member[CommonTokens].Member[Class].Instance.Method[foo];test-source", //
|
||||
// testing non-positional arguments
|
||||
"testlib;;Member[ArgPos].Member[MyClass].Subclass.Member[foo].Parameter[self];test-source", //
|
||||
"testlib;;Member[ArgPos].Member[MyClass].Subclass.Member[foo].Parameter[named:];test-source", //
|
||||
"testlib;;Member[ArgPos].Member[MyClass].Subclass.Member[secondAndAfter].Parameter[1..];test-source", //
|
||||
"testlib;;Member[ArgPos].Member[MyClass].Subclass.Member[otherSelfTest].Parameter[0];test-source", //
|
||||
"testlib;;Member[ArgPos].Member[MyClass].Subclass.Member[anyParam].Parameter[any];test-source", //
|
||||
"testlib;;Member[ArgPos].Member[MyClass].Subclass.Member[anyNamed].Parameter[any-named];test-source", //
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class BasicTaintTracking extends TaintTracking::Configuration {
|
||||
BasicTaintTracking() { this = "BasicTaintTracking" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) {
|
||||
source = ModelOutput::getASourceNode("test-source").getAnImmediateUse()
|
||||
}
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) {
|
||||
sink = ModelOutput::getASinkNode("test-sink").getARhs()
|
||||
}
|
||||
}
|
||||
|
||||
query predicate taintFlow(DataFlow::Node source, DataFlow::Node sink) {
|
||||
any(BasicTaintTracking tr).hasFlow(source, sink)
|
||||
}
|
||||
|
||||
query predicate isSink(DataFlow::Node node, string kind) {
|
||||
node = ModelOutput::getASinkNode(kind).getARhs()
|
||||
}
|
||||
|
||||
query predicate isSource(DataFlow::Node node, string kind) {
|
||||
node = ModelOutput::getASourceNode(kind).getAnImmediateUse()
|
||||
}
|
||||
|
||||
class SyntaxErrorTest extends ModelInput::SinkModelCsv {
|
||||
override predicate row(string row) {
|
||||
row =
|
||||
[
|
||||
"testlib;;Member[foo],Member[bar];test-sink", "testlib;;Member[foo] Member[bar];test-sink",
|
||||
"testlib;;Member[foo]. Member[bar];test-sink",
|
||||
"testlib;;Member[foo], Member[bar];test-sink",
|
||||
"testlib;;Member[foo]..Member[bar];test-sink",
|
||||
"testlib;;Member[foo] .Member[bar];test-sink", "testlib;;Member[foo]Member[bar];test-sink",
|
||||
"testlib;;Member[foo;test-sink", "testlib;;Member[foo]];test-sink",
|
||||
"testlib;;Member[foo]].Member[bar];test-sink"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
query predicate syntaxErrors(AccessPathSyntax::AccessPath path) { path.hasSyntaxError() }
|
||||
|
||||
query predicate warning = ModelOutput::getAWarning/0;
|
||||
@@ -0,0 +1,7 @@
|
||||
| CSV type row should have 5 columns but has 2: test;TooFewColumns |
|
||||
| CSV type row should have 5 columns but has 8: test;TooManyColumns;;;Member[Foo].Instance;too;many;columns |
|
||||
| Invalid argument '0-1' in token 'Argument[0-1]' in access path: Method[foo].Argument[0-1] |
|
||||
| Invalid argument '*' in token 'Argument[*]' in access path: Method[foo].Argument[*] |
|
||||
| Invalid token 'Argument' is missing its arguments, in access path: Method[foo].Argument |
|
||||
| Invalid token 'Member' is missing its arguments, in access path: Method[foo].Member |
|
||||
| Invalid token name 'Arg' in access path: Method[foo].Arg[0] |
|
||||
25
python/ql/test/library-tests/frameworks/data/warnings.ql
Normal file
25
python/ql/test/library-tests/frameworks/data/warnings.ql
Normal file
@@ -0,0 +1,25 @@
|
||||
import python
|
||||
import semmle.python.frameworks.data.internal.AccessPathSyntax as AccessPathSyntax
|
||||
import semmle.python.frameworks.data.internal.ApiGraphModels as ApiGraphModels
|
||||
import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
private class InvalidTypeModel extends ModelInput::TypeModelCsv {
|
||||
override predicate row(string row) {
|
||||
row =
|
||||
[
|
||||
"test;TooManyColumns;;;Member[Foo].Instance;too;many;columns", //
|
||||
"test;TooFewColumns", //
|
||||
"test;X;test;Y;Method[foo].Arg[0]", //
|
||||
"test;X;test;Y;Method[foo].Argument[0-1]", //
|
||||
"test;X;test;Y;Method[foo].Argument[*]", //
|
||||
"test;X;test;Y;Method[foo].Argument", //
|
||||
"test;X;test;Y;Method[foo].Member", //
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class IsTesting extends ApiGraphModels::TestAllModels {
|
||||
IsTesting() { this = this }
|
||||
}
|
||||
|
||||
query predicate warning = ModelOutput::getAWarning/0;
|
||||
@@ -71,6 +71,7 @@ def test_taint(request: HttpRequest, foo, bar, baz=None): # $requestHandler rou
|
||||
request.FILES["key"].name, # $ tainted
|
||||
request.FILES["key"].file, # $ tainted
|
||||
request.FILES["key"].file.read(), # $ tainted
|
||||
request.FILES["key"].read(), # $ tainted
|
||||
|
||||
request.FILES.get("key"), # $ tainted
|
||||
request.FILES.get("key").name, # $ tainted
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import httpx
|
||||
import ssl
|
||||
|
||||
httpx.get("url") # $ clientRequestUrlPart="url"
|
||||
httpx.post("url") # $ clientRequestUrlPart="url"
|
||||
@@ -15,10 +16,30 @@ response = client.options("url") # $ clientRequestUrlPart="url"
|
||||
response = client.request("method", url="url") # $ clientRequestUrlPart="url"
|
||||
response = client.stream("method", url="url") # $ clientRequestUrlPart="url"
|
||||
|
||||
client = httpx.AsyncClient()
|
||||
response = client.get("url") # $ clientRequestUrlPart="url"
|
||||
response = client.post("url") # $ clientRequestUrlPart="url"
|
||||
response = client.patch("url") # $ clientRequestUrlPart="url"
|
||||
response = client.options("url") # $ clientRequestUrlPart="url"
|
||||
response = client.request("method", url="url") # $ clientRequestUrlPart="url"
|
||||
response = client.stream("method", url="url") # $ clientRequestUrlPart="url"
|
||||
async def async_test():
|
||||
client = httpx.AsyncClient()
|
||||
response = await client.get("url") # $ clientRequestUrlPart="url"
|
||||
response = await client.post("url") # $ clientRequestUrlPart="url"
|
||||
response = await client.patch("url") # $ clientRequestUrlPart="url"
|
||||
response = await client.options("url") # $ clientRequestUrlPart="url"
|
||||
response = await client.request("method", url="url") # $ clientRequestUrlPart="url"
|
||||
response = await client.stream("method", url="url") # $ clientRequestUrlPart="url"
|
||||
|
||||
# ==============================================================================
|
||||
# Disabling certificate validation
|
||||
# ==============================================================================
|
||||
|
||||
httpx.get("url", verify=False) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
httpx.get("url", verify=0) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
httpx.get("url", verify=None) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
|
||||
# A manually constructed SSLContext does not have safe defaults, so is effectively the
|
||||
# same as turning off SSL validation
|
||||
context = ssl.SSLContext()
|
||||
assert context.check_hostname == False
|
||||
assert context.verify_mode == ssl.VerifyMode.CERT_NONE
|
||||
|
||||
httpx.get("url", verify=context) # $ clientRequestUrlPart="url" MISSING: clientRequestCertValidationDisabled
|
||||
|
||||
client = httpx.Client(verify=False)
|
||||
client.get("url") # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
|
||||
@@ -33,6 +33,10 @@ resp = requests.options("url") # $ clientRequestUrlPart="url"
|
||||
# ==============================================================================
|
||||
|
||||
resp = requests.get("url", verify=False) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
resp = requests.get("url", verify=0) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
|
||||
# in reuqests, using `verify=None` is just the default value, so does NOT turn off certificate validation
|
||||
resp = requests.get("url", verify=None) # $ clientRequestUrlPart="url"
|
||||
|
||||
def make_get(verify_arg):
|
||||
resp = requests.get("url", verify=verify_arg) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import urllib2
|
||||
import ssl
|
||||
|
||||
resp = urllib2.Request("url") # $ clientRequestUrlPart="url"
|
||||
resp = urllib2.Request(url="url") # $ clientRequestUrlPart="url"
|
||||
|
||||
resp = urllib2.urlopen("url") # $ clientRequestUrlPart="url"
|
||||
resp = urllib2.urlopen(url="url") # $ clientRequestUrlPart="url"
|
||||
resp = urllib2.urlopen(url="url") # $ clientRequestUrlPart="url"
|
||||
|
||||
# ==============================================================================
|
||||
# Certificate validation disabled
|
||||
# ==============================================================================
|
||||
|
||||
# A manually constructed SSLContext does not have safe defaults, so is effectively the
|
||||
# same as turning off SSL validation
|
||||
context = ssl.SSLContext()
|
||||
assert context.check_hostname == False
|
||||
assert context.verify_mode == ssl.VerifyMode.CERT_NONE
|
||||
|
||||
urllib2.urlopen("url", context=context) # $ clientRequestUrlPart="url" MISSING: clientRequestCertValidationDisabled
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import ssl
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
Request("url") # $ clientRequestUrlPart="url"
|
||||
Request(url="url") # $ clientRequestUrlPart="url"
|
||||
|
||||
urlopen("url") # $ clientRequestUrlPart="url"
|
||||
urlopen(url="url") # $ clientRequestUrlPart="url"
|
||||
urlopen(url="url") # $ clientRequestUrlPart="url"
|
||||
|
||||
# ==============================================================================
|
||||
# Certificate validation disabled
|
||||
# ==============================================================================
|
||||
|
||||
# A manually constructed SSLContext does not have safe defaults, so is effectively the
|
||||
# same as turning off SSL validation
|
||||
context = ssl.SSLContext()
|
||||
assert context.check_hostname == False
|
||||
assert context.verify_mode == ssl.VerifyMode.CERT_NONE
|
||||
|
||||
urlopen("url", context=context) # $ clientRequestUrlPart="url" MISSING: clientRequestCertValidationDisabled
|
||||
|
||||
@@ -27,3 +27,33 @@ resp = pool.request("method", "url") # $ clientRequestUrlPart="url"
|
||||
resp = pool.request("method", url="url") # $ clientRequestUrlPart="url"
|
||||
resp = pool.urlopen("method", "url") # $ clientRequestUrlPart="url"
|
||||
resp = pool.urlopen("method", url="url") # $ clientRequestUrlPart="url"
|
||||
|
||||
# ==============================================================================
|
||||
# Certificate validation disabled
|
||||
# ==============================================================================
|
||||
|
||||
# see https://docs.python.org/3.10/library/ssl.html#ssl.CERT_NONE
|
||||
pool = urllib3.HTTPSConnectionPool("host", cert_reqs='CERT_NONE')
|
||||
resp = pool.request("method", "url") # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
|
||||
pool = urllib3.HTTPSConnectionPool("host", assert_hostname=False)
|
||||
resp = pool.request("method", "url") # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
|
||||
pool = urllib3.HTTPSConnectionPool("host", cert_reqs='CERT_NONE', assert_hostname=False)
|
||||
resp = pool.request("method", "url") # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
|
||||
|
||||
# same with PoolManager
|
||||
pool = urllib3.PoolManager(cert_reqs='CERT_NONE')
|
||||
resp = pool.request("method", "url") # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
|
||||
pool = urllib3.PoolManager(assert_hostname=False)
|
||||
resp = pool.request("method", "url") # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
|
||||
|
||||
# and same with ProxyManager
|
||||
pool = urllib3.ProxyManager("https://proxy", cert_reqs='CERT_NONE')
|
||||
resp = pool.request("method", "url") # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
|
||||
pool = urllib3.ProxyManager("https://proxy", assert_hostname=False)
|
||||
resp = pool.request("method", "url") # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
| make_request.py:5:1:5:48 | ControlFlowNode for Attribute() | Call to requests.get with verify=$@ | make_request.py:5:43:5:47 | ControlFlowNode for False | False |
|
||||
| make_request.py:7:1:7:49 | ControlFlowNode for Attribute() | Call to requests.post with verify=$@ | make_request.py:7:44:7:48 | ControlFlowNode for False | False |
|
||||
| make_request.py:12:1:12:39 | ControlFlowNode for put() | Call to requests.put with verify=$@ | make_request.py:12:34:12:38 | ControlFlowNode for False | False |
|
||||
| make_request.py:28:5:28:46 | ControlFlowNode for patch() | Call to requests.patch with verify=$@ | make_request.py:30:6:30:10 | ControlFlowNode for False | False |
|
||||
| make_request.py:34:1:34:45 | ControlFlowNode for Attribute() | Call to requests.post with verify=$@ | make_request.py:34:44:34:44 | ControlFlowNode for IntegerLiteral | False |
|
||||
| make_request.py:5:1:5:48 | ControlFlowNode for Attribute() | This request may run without certificate validation because it is $@. | make_request.py:5:43:5:47 | ControlFlowNode for False | disabled here | make_request.py:5:43:5:47 | ControlFlowNode for False | here |
|
||||
| make_request.py:7:1:7:49 | ControlFlowNode for Attribute() | This request may run without certificate validation because it is $@. | make_request.py:7:44:7:48 | ControlFlowNode for False | disabled here | make_request.py:7:44:7:48 | ControlFlowNode for False | here |
|
||||
| make_request.py:12:1:12:39 | ControlFlowNode for put() | This request may run without certificate validation because it is $@. | make_request.py:12:34:12:38 | ControlFlowNode for False | disabled here | make_request.py:12:34:12:38 | ControlFlowNode for False | here |
|
||||
| make_request.py:28:5:28:46 | ControlFlowNode for patch() | This request may run without certificate validation because it is $@ by the value from $@. | make_request.py:28:40:28:45 | ControlFlowNode for verify | disabled here | make_request.py:30:6:30:10 | ControlFlowNode for False | here |
|
||||
| make_request.py:34:1:34:45 | ControlFlowNode for Attribute() | This request may run without certificate validation because it is $@. | make_request.py:34:44:34:44 | ControlFlowNode for IntegerLiteral | disabled here | make_request.py:34:44:34:44 | ControlFlowNode for IntegerLiteral | here |
|
||||
| make_request.py:41:1:41:26 | ControlFlowNode for Attribute() | This request may run without certificate validation because it is $@. | make_request.py:41:21:41:25 | ControlFlowNode for False | disabled here | make_request.py:41:21:41:25 | ControlFlowNode for False | here |
|
||||
|
||||
@@ -36,3 +36,6 @@ requests.post('https://semmle.com', verify=0) # BAD
|
||||
# requests treat `None` as default value, which means it is turned on
|
||||
requests.get('https://semmle.com') # OK
|
||||
requests.get('https://semmle.com', verify=None) # OK
|
||||
|
||||
s = requests.Session()
|
||||
s.get("url", verify=False) # BAD
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
| test_cryptodome.py:11:13:11:42 | ControlFlowNode for Attribute() | The cryptographic algorithm ARC4 is broken or weak, and should not be used. |
|
||||
| test_cryptodome.py:16:13:16:42 | ControlFlowNode for Attribute() | The block mode ECB is broken or weak, and should not be used. |
|
||||
| test_cryptography.py:13:13:13:44 | ControlFlowNode for Attribute() | The cryptographic algorithm ARC4 is broken or weak, and should not be used. |
|
||||
| test_cryptography.py:22:13:22:58 | ControlFlowNode for Attribute() | The block mode ECB is broken or weak, and should not be used. |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# snippet from python/ql/test/experimental/library-tests/frameworks/cryptodome/test_rc4.py
|
||||
from Cryptodome.Cipher import ARC4
|
||||
from Cryptodome.Cipher import ARC4, AES
|
||||
|
||||
import os
|
||||
|
||||
@@ -11,3 +11,8 @@ cipher = ARC4.new(key)
|
||||
encrypted = cipher.encrypt(secret_message) # NOT OK
|
||||
|
||||
print(secret_message, encrypted)
|
||||
|
||||
cipher = AES.new(key, AES.MODE_ECB)
|
||||
encrypted = cipher.encrypt(secret_message) # NOT OK
|
||||
|
||||
print(secret_message, encrypted)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# snippet from python/ql/test/experimental/library-tests/frameworks/cryptography/test_rc4.py
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms, Cipher
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher
|
||||
import os
|
||||
|
||||
key = os.urandom(256//8)
|
||||
@@ -14,3 +14,12 @@ encrypted = encryptor.update(secret_message) # NOT OK
|
||||
encrypted += encryptor.finalize()
|
||||
|
||||
print(secret_message, encrypted)
|
||||
|
||||
algorithm = algorithms.AES(key)
|
||||
cipher = Cipher(algorithm, mode=modes.ECB())
|
||||
|
||||
encryptor = cipher.encryptor()
|
||||
encrypted = encryptor.update(secret_message + b'\x80\x00') # NOT OK
|
||||
encrypted += encryptor.finalize()
|
||||
|
||||
print(secret_message, encrypted)
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
| test.py:8:1:8:20 | ControlFlowNode for Attribute() | Overly permissive mask in chmod sets file to world writable. |
|
||||
| test.py:9:1:9:21 | ControlFlowNode for Attribute() | Overly permissive mask in chmod sets file to world writable. |
|
||||
| test.py:11:1:11:21 | ControlFlowNode for Attribute() | Overly permissive mask in chmod sets file to group readable. |
|
||||
| test.py:13:1:13:28 | ControlFlowNode for Attribute() | Overly permissive mask in chmod sets file to group writable. |
|
||||
| test.py:14:1:14:19 | ControlFlowNode for Attribute() | Overly permissive mask in chmod sets file to group writable. |
|
||||
| test.py:16:1:16:25 | ControlFlowNode for Attribute() | Overly permissive mask in open sets file to world readable. |
|
||||
|
||||
@@ -1 +1 @@
|
||||
semmle-extractor-options: --max-import-depth=2 -p ../lib
|
||||
semmle-extractor-options: --max-import-depth=2 -p ../lib --lang=3
|
||||
|
||||
@@ -61,8 +61,8 @@ def ok_assert_false(x):
|
||||
if x:
|
||||
assert 0==1, "Ok"
|
||||
|
||||
class TestCase:
|
||||
pass
|
||||
from unittest import TestCase
|
||||
|
||||
|
||||
class MyTest(TestCase):
|
||||
def test_ok_assert_in_test(self, x):
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
| test.py:36:21:36:33 | Comment # lgtm method | lgtm method | lgtm | test.py:36:1:36:33 | suppression range |
|
||||
| test.py:39:4:39:8 | Comment #noqa | noqa | lgtm | test.py:39:1:39:8 | suppression range |
|
||||
| test.py:40:4:40:9 | Comment # noqa | noqa | lgtm | test.py:40:1:40:9 | suppression range |
|
||||
| test.py:45:4:45:31 | Comment # noqa -- Some extra detail. | noqa -- Some extra detail. | lgtm | test.py:45:1:45:31 | suppression range |
|
||||
| test.py:50:34:50:117 | Comment # noqa: E501; (line too long) pylint: disable=invalid-name; lgtm [py/missing-equals] | noqa: E501; (line too long) pylint: disable=invalid-name; lgtm [py/missing-equals] | lgtm [py/missing-equals] | test.py:50:1:50:117 | suppression range |
|
||||
| test.py:52:4:52:67 | Comment # noqa: E501; (line too long) pylint: disable=invalid-name; lgtm | noqa: E501; (line too long) pylint: disable=invalid-name; lgtm | lgtm | test.py:52:1:52:67 | suppression range |
|
||||
| test.py:53:4:53:78 | Comment # random nonsense lgtm [py/missing-equals] and then some more commentary... | random nonsense lgtm [py/missing-equals] and then some more commentary... | lgtm [py/missing-equals] | test.py:53:1:53:78 | suppression range |
|
||||
|
||||
Reference in New Issue
Block a user