Merge branch main into rc/3.6

This commit is contained in:
Edoardo Pirovano
2022-06-21 11:15:25 +01:00
1296 changed files with 64800 additions and 9671 deletions

View File

@@ -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() }

View File

@@ -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
}

View File

@@ -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

View File

@@ -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) }
}
/**

View File

@@ -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"] }

View File

@@ -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, _, _)
)
}

View File

@@ -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, _, _)
)
}

View File

@@ -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, _, _)
)
}

View File

@@ -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, _, _)
)
}

View File

@@ -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()
)
}
}

View File

@@ -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
}
}
}

View File

@@ -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()

View File

@@ -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() }
}
}

View File

@@ -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() }
}
}
}

View File

@@ -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")
}
}
}

View 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
)
}
}
}

View File

@@ -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. */

View File

@@ -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() }
}
}

View File

@@ -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() }
}
/**

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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
)
}
}
}

View 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")
}
}

View File

@@ -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))) }
}

View File

@@ -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
)
}
}

View File

@@ -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
)
}

View File

@@ -4,3 +4,4 @@
*/
import semmle.python.dataflow.new.DataFlow
import semmle.python.concepts.CryptoAlgorithms as CryptoAlgorithms

View File

@@ -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" }
}
}

View File

@@ -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.
*/

View File

@@ -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() }
}
}

View File

@@ -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))
)
}

View File

@@ -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"

View File

@@ -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."

View File

@@ -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 + "."

View File

@@ -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" }
}

View File

@@ -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.

View File

@@ -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`.

View 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"

View 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()

View File

@@ -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>

View 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"

View File

@@ -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()] }
}

View File

@@ -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

View 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")]
}
}
}
}

View File

@@ -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")]
}
}
}
}
}

View 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")
}
}
}

View File

@@ -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") }
}
}

View 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")]
}
}
}

View File

@@ -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)
)
}
}

View File

@@ -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
}
}

View File

@@ -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"`.

View File

@@ -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() |

View File

@@ -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"
)
)
}

View 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
)
}
}

View File

@@ -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 |

View File

@@ -0,0 +1 @@
experimental/Security/CWE-079/ReflectedXSS.ql

View File

@@ -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"))

View File

@@ -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)

View File

@@ -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())

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 |

View File

@@ -0,0 +1 @@
experimental/Security/CWE-1236/CsvInjection.ql

View File

@@ -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()

View File

@@ -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 |

View File

@@ -1,10 +0,0 @@
class TestCase:
pass
class MyTest(TestCase):
def test_1(self):
pass
def test_2(self):
pass

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,2 @@
import python
import experimental.meta.MaDTest

View File

@@ -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)

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View 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

View 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

View 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;

View File

@@ -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] |

View 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;

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 |

View File

@@ -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

View File

@@ -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. |

View File

@@ -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)

View File

@@ -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)

View File

@@ -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. |

View File

@@ -1 +1 @@
semmle-extractor-options: --max-import-depth=2 -p ../lib
semmle-extractor-options: --max-import-depth=2 -p ../lib --lang=3

View File

@@ -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):

View File

@@ -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 |