Skip to content

Python: port modification of default value #6557

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e3765ce
Python: Add tests for modification of defaults
yoff Aug 23, 2021
e865a29
Python: straight port of query
yoff Aug 24, 2021
5bff518
Python: switch from negative to positive list
yoff Aug 25, 2021
8614563
Python: More tests of syntactic constructs
yoff Aug 26, 2021
d834cec
Python: test simple sanitizer
yoff Aug 26, 2021
097c23e
Python: add inline expectations test
yoff Aug 26, 2021
49ae549
Python: Implement modifying syntax
yoff Aug 26, 2021
a762373
Python: Implement simple barrier guard
yoff Aug 30, 2021
1903cb8
Python: Add change note
yoff Aug 30, 2021
0de621e
Python: Add qldoc
yoff Aug 30, 2021
a855074
Python: Try to remove py2/3 differences
yoff Aug 30, 2021
c6eb795
Apply suggestions from code review
yoff Sep 3, 2021
913990b
Python: Add suggested comments and test case
yoff Sep 3, 2021
4998a48
Python: Fix simple guards
yoff Sep 6, 2021
ae8408b
Python: Add missing qldoc
yoff Sep 7, 2021
29cb067
Python: Remember to update test expectations
yoff Sep 7, 2021
8729701
Merge branch 'main' of github.com:github/codeql into python/port-modi…
yoff Sep 7, 2021
b48caaf
Python: fix reference to PrintNode.qll
yoff Sep 7, 2021
e8644f6
Python: coment out discriminating test
yoff Sep 7, 2021
43effd2
Update python/ql/src/semmle/python/functions/ModificationOfParameterW…
yoff Sep 7, 2021
b20232d
Python: Simplify guards as suggested
yoff Sep 10, 2021
7cfa08a
Python: Do not use BarrierGuards
yoff Sep 10, 2021
02fd63c
Merge branch 'main' of github.com:github/codeql into python/port-modi…
yoff Sep 10, 2021
2eb1173
Python: Subpaths in test output
yoff Sep 10, 2021
758b6bd
Update python/ql/src/semmle/python/functions/ModificationOfParameterW…
yoff Sep 15, 2021
8ea7a28
Python: Unexpose fields as suggested.
yoff Sep 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
lgtm,codescanning
* Updated _Modification of parameter with default_ (`py/modification-of-default-value`) query to use the new data flow library instead of the old taint tracking library and to remove the use of points-to analysis. You may see differences in the results found by the query, but overall this change should result in a more robust and accurate analysis.
92 changes: 8 additions & 84 deletions python/ql/src/Functions/ModificationOfParameterWithDefault.ql
Original file line number Diff line number Diff line change
Expand Up @@ -12,88 +12,12 @@
*/

import python
import semmle.python.security.Paths

predicate safe_method(string name) {
name = "count" or
name = "index" or
name = "copy" or
name = "get" or
name = "has_key" or
name = "items" or
name = "keys" or
name = "values" or
name = "iteritems" or
name = "iterkeys" or
name = "itervalues" or
name = "__contains__" or
name = "__getitem__" or
name = "__getattribute__"
}

/** Gets the truthiness (non emptyness) of the default of `p` if that value is mutable */
private boolean mutableDefaultValue(Parameter p) {
exists(Dict d | p.getDefault() = d |
exists(d.getAKey()) and result = true
or
not exists(d.getAKey()) and result = false
)
or
exists(List l | p.getDefault() = l |
exists(l.getAnElt()) and result = true
or
not exists(l.getAnElt()) and result = false
)
}

class NonEmptyMutableValue extends TaintKind {
NonEmptyMutableValue() { this = "non-empty mutable value" }
}

class EmptyMutableValue extends TaintKind {
EmptyMutableValue() { this = "empty mutable value" }

override boolean booleanValue() { result = false }
}

class MutableDefaultValue extends TaintSource {
boolean nonEmpty;

MutableDefaultValue() { nonEmpty = mutableDefaultValue(this.(NameNode).getNode()) }

override string toString() { result = "mutable default value" }

override predicate isSourceOf(TaintKind kind) {
nonEmpty = false and kind instanceof EmptyMutableValue
or
nonEmpty = true and kind instanceof NonEmptyMutableValue
}
}

private ClassValue mutable_class() {
result = Value::named("list") or
result = Value::named("dict")
}

class Mutation extends TaintSink {
Mutation() {
exists(AugAssign a | a.getTarget().getAFlowNode() = this)
or
exists(Call c, Attribute a | c.getFunc() = a |
a.getObject().getAFlowNode() = this and
not safe_method(a.getName()) and
this.(ControlFlowNode).pointsTo().getClass() = mutable_class()
)
}

override predicate sinks(TaintKind kind) {
kind instanceof EmptyMutableValue
or
kind instanceof NonEmptyMutableValue
}
}

from TaintedPathSource src, TaintedPathSink sink
where src.flowsTo(sink)
select sink.getSink(), src, sink, "$@ flows to here and is mutated.", src.getSource(),
import semmle.python.functions.ModificationOfParameterWithDefault
import DataFlow::PathGraph

from
ModificationOfParameterWithDefault::Configuration config, DataFlow::PathNode source,
DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ flows to here and is mutated.", source.getNode(),
"Default value"
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Provides a data-flow configuration for detecting modifications of a parameters default value.
*
* Note, for performance reasons: only import this file if
* `ModificationOfParameterWithDefault::Configuration` is needed, otherwise
* `ModificationOfParameterWithDefaultCustomizations` should be imported instead.
*/

private import python
import semmle.python.dataflow.new.DataFlow

/**
* Provides a data-flow configuration for detecting modifications of a parameters default value.
*/
module ModificationOfParameterWithDefault {
import ModificationOfParameterWithDefaultCustomizations::ModificationOfParameterWithDefault

/**
* A data-flow configuration for detecting modifications of a parameters default value.
*/
class Configuration extends DataFlow::Configuration {
/** Record whether the default value being tracked is non-empty. */
boolean nonEmptyDefault;

Configuration() {
nonEmptyDefault in [true, false] and
this = "ModificationOfParameterWithDefault:" + nonEmptyDefault.toString()
}

override predicate isSource(DataFlow::Node source) {
source.(Source).isNonEmpty() = nonEmptyDefault
}

override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }

override predicate isBarrier(DataFlow::Node node) {
// if we are tracking a non-empty default, then it is ok to modify empty values,
// so our tracking ends at those.
nonEmptyDefault = true and node instanceof MustBeEmpty
or
// if we are tracking a empty default, then it is ok to modify non-empty values,
// so our tracking ends at those.
nonEmptyDefault = false and node instanceof MustBeNonEmpty
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/**
* Provides default sources, sinks and sanitizers for detecting
* modifications of a parameters default value, as well as extension points for adding your own.
*/

private import python
private import semmle.python.dataflow.new.DataFlow
private import semmle.python.dataflow.new.BarrierGuards

/**
* Provides default sources, sinks and sanitizers for detecting
* "command injection"
* vulnerabilities, as well as extension points for adding your own.
*/
module ModificationOfParameterWithDefault {
/**
* A data flow source for detecting modifications of a parameters default value,
* that is a default value for some parameter.
*/
abstract class Source extends DataFlow::Node {
/** Result is true if the default value is non-empty for this source and false if not. */
abstract boolean isNonEmpty();
}

/**
* A data flow sink for detecting modifications of a parameters default value,
* that is a node representing a modification.
*/
abstract class Sink extends DataFlow::Node { }

/**
* A sanitizer for detecting modifications of a parameters default value
* should determine if the node (which is perhaps about to be modified)
* can be the default value or not.
*
* In this query we do not track the default value exactly, but rather wheter
* it is empty or not (see `Source`).
*
* This is the extension point for determining that a node must be empty and
* therefor is allowed to be modified if the tracked default value is non-empty.
*/
abstract class MustBeEmpty extends DataFlow::Node { }

/**
* A sanitizer for detecting modifications of a parameters default value
* should determine if the node (which is perhaps about to be modified)
* can be the default value or not.
*
* In this query we do not track the default value exactly, but rather wheter
* it is empty or not (see `Source`).
*
* This is the extension point for determining that a node must be non-empty
* and therefor is allowed to be modified if the tracked default value is empty.
*/
abstract class MustBeNonEmpty extends DataFlow::Node { }

/** Gets the truthiness (non emptyness) of the default of `p` if that value is mutable */
private boolean mutableDefaultValue(Parameter p) {
exists(Dict d | p.getDefault() = d |
exists(d.getAKey()) and result = true
or
not exists(d.getAKey()) and result = false
)
or
exists(List l | p.getDefault() = l |
exists(l.getAnElt()) and result = true
or
not exists(l.getAnElt()) and result = false
)
}

/**
* A mutable default value for a parameter, considered as a flow source.
*/
class MutableDefaultValue extends Source {
boolean nonEmpty;

MutableDefaultValue() { nonEmpty = mutableDefaultValue(this.asCfgNode().(NameNode).getNode()) }

override boolean isNonEmpty() { result = nonEmpty }
}

/**
* A name of a list function that modifies the list.
* See https://docs.python.org/3/tutorial/datastructures.html#more-on-lists
*/
string list_modifying_method() {
result in ["append", "extend", "insert", "remove", "pop", "clear", "sort", "reverse"]
}

/**
* A name of a dict function that modifies the dict.
* See https://docs.python.org/3/library/stdtypes.html#dict
*/
string dict_modifying_method() { result in ["clear", "pop", "popitem", "setdefault", "update"] }

/**
* A mutation of the default value is a flow sink.
*
* Syntactic constructs that modify a list are:
* - s[i] = x
* - s[i:j] = t
* - del s[i:j]
* - s[i:j:k] = t
* - del s[i:j:k]
* - s += t
* - s *= n
* See https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types
*
* Syntactic constructs that modify a dictionary are:
* - d[key] = value
* - del d[key]
* - d |= other
* See https://docs.python.org/3/library/stdtypes.html#dict
*
* These are all covered by:
* - assignment to a subscript (includes slices)
* - deletion of a subscript
* - augmented assignment to the value
*/
class Mutation extends Sink {
Mutation() {
// assignment to a subscript (includes slices)
exists(DefinitionNode d | d.(SubscriptNode).getObject() = this.asCfgNode())
or
// deletion of a subscript
exists(DeletionNode d | d.getTarget().(SubscriptNode).getObject() = this.asCfgNode())
or
// augmented assignment to the value
exists(AugAssign a | a.getTarget().getAFlowNode() = this.asCfgNode())
or
// modifying function call
exists(DataFlow::CallCfgNode c, DataFlow::AttrRead a | c.getFunction() = a |
a.getObject() = this and
a.getAttributeName() in [list_modifying_method(), dict_modifying_method()]
)
}
}

// This to reimplement some of the functionality of the DataFlow::BarrierGuard
private import semmle.python.essa.SsaCompute

/**
* A data-flow node that is known to be either truthy or falsey.
*
* It handles the cases `if x` and `if not x`.
*
* For example, in the following code, `this` will be the `x` that is printed,
* which we will know is truthy:
*
* ```py
* if x:
* print(x)
* ```
*/
private class MustBe extends DataFlow::Node {
boolean truthy;

MustBe() {
exists(DataFlow::GuardNode guard, NameNode guarded, boolean branch |
// case: if x
guard = guarded and
branch = truthy
or
// case: if not x
guard.(UnaryExprNode).getNode().getOp() instanceof Not and
guarded = guard.(UnaryExprNode).getOperand() and
branch = truthy.booleanNot()
|
// guard controls this
guard.controlsBlock(this.asCfgNode().getBasicBlock(), branch) and
// there is a definition tying the guarded value to this
exists(EssaDefinition def |
AdjacentUses::useOfDef(def, this.asCfgNode()) and
AdjacentUses::useOfDef(def, guarded)
)
)
}
}

/** Simple guard detecting truthy values. */
private class MustBeTruthy extends MustBe, MustBeNonEmpty {
MustBeTruthy() { truthy = true }
}

/** Simple guard detecting falsey values. */
private class MustBeFalsey extends MustBe, MustBeEmpty {
MustBeFalsey() { truthy = false }
}
}
Loading