diff --git a/python/change-notes/2021-08-30-port-modifying-default-query.md b/python/change-notes/2021-08-30-port-modifying-default-query.md new file mode 100644 index 000000000000..2fcd9a11ded1 --- /dev/null +++ b/python/change-notes/2021-08-30-port-modifying-default-query.md @@ -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. diff --git a/python/ql/src/Functions/ModificationOfParameterWithDefault.ql b/python/ql/src/Functions/ModificationOfParameterWithDefault.ql index 03edc35fa17c..9ff89d30e3fa 100644 --- a/python/ql/src/Functions/ModificationOfParameterWithDefault.ql +++ b/python/ql/src/Functions/ModificationOfParameterWithDefault.ql @@ -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" diff --git a/python/ql/src/semmle/python/functions/ModificationOfParameterWithDefault.qll b/python/ql/src/semmle/python/functions/ModificationOfParameterWithDefault.qll new file mode 100644 index 000000000000..24103a33aceb --- /dev/null +++ b/python/ql/src/semmle/python/functions/ModificationOfParameterWithDefault.qll @@ -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 + } + } +} diff --git a/python/ql/src/semmle/python/functions/ModificationOfParameterWithDefaultCustomizations.qll b/python/ql/src/semmle/python/functions/ModificationOfParameterWithDefaultCustomizations.qll new file mode 100644 index 000000000000..6abb3f800858 --- /dev/null +++ b/python/ql/src/semmle/python/functions/ModificationOfParameterWithDefaultCustomizations.qll @@ -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 } + } +} diff --git a/python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/ModificationOfParameterWithDefault.expected b/python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/ModificationOfParameterWithDefault.expected new file mode 100644 index 000000000000..86daf9cb09ea --- /dev/null +++ b/python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/ModificationOfParameterWithDefault.expected @@ -0,0 +1,108 @@ +edges +| test.py:2:12:2:12 | ControlFlowNode for l | test.py:3:5:3:5 | ControlFlowNode for l | +| test.py:7:11:7:11 | ControlFlowNode for l | test.py:8:5:8:5 | ControlFlowNode for l | +| test.py:12:14:12:14 | ControlFlowNode for l | test.py:13:9:13:9 | ControlFlowNode for l | +| test.py:17:15:17:15 | ControlFlowNode for l | test.py:18:5:18:5 | ControlFlowNode for l | +| test.py:22:15:22:15 | ControlFlowNode for l | test.py:23:5:23:5 | ControlFlowNode for l | +| test.py:27:12:27:12 | ControlFlowNode for l | test.py:28:5:28:5 | ControlFlowNode for l | +| test.py:38:13:38:13 | ControlFlowNode for l | test.py:39:5:39:5 | ControlFlowNode for l | +| test.py:43:14:43:14 | ControlFlowNode for l | test.py:44:13:44:13 | ControlFlowNode for l | +| test.py:44:13:44:13 | ControlFlowNode for l | test.py:38:13:38:13 | ControlFlowNode for l | +| test.py:48:14:48:14 | ControlFlowNode for l | test.py:49:5:49:5 | ControlFlowNode for l | +| test.py:53:10:53:10 | ControlFlowNode for d | test.py:54:5:54:5 | ControlFlowNode for d | +| test.py:58:19:58:19 | ControlFlowNode for d | test.py:59:5:59:5 | ControlFlowNode for d | +| test.py:63:28:63:28 | ControlFlowNode for d | test.py:64:5:64:5 | ControlFlowNode for d | +| test.py:67:14:67:14 | ControlFlowNode for d | test.py:68:5:68:5 | ControlFlowNode for d | +| test.py:72:19:72:19 | ControlFlowNode for d | test.py:73:14:73:14 | ControlFlowNode for d | +| test.py:73:14:73:14 | ControlFlowNode for d | test.py:67:14:67:14 | ControlFlowNode for d | +| test.py:77:17:77:17 | ControlFlowNode for d | test.py:78:5:78:5 | ControlFlowNode for d | +| test.py:82:26:82:26 | ControlFlowNode for d | test.py:83:5:83:5 | ControlFlowNode for d | +| test.py:87:35:87:35 | ControlFlowNode for d | test.py:88:5:88:5 | ControlFlowNode for d | +| test.py:91:21:91:21 | ControlFlowNode for d | test.py:92:5:92:5 | ControlFlowNode for d | +| test.py:96:26:96:26 | ControlFlowNode for d | test.py:97:21:97:21 | ControlFlowNode for d | +| test.py:97:21:97:21 | ControlFlowNode for d | test.py:91:21:91:21 | ControlFlowNode for d | +| test.py:108:14:108:14 | ControlFlowNode for d | test.py:109:9:109:9 | ControlFlowNode for d | +| test.py:113:20:113:20 | ControlFlowNode for d | test.py:115:5:115:5 | ControlFlowNode for d | +| test.py:119:29:119:29 | ControlFlowNode for d | test.py:121:5:121:5 | ControlFlowNode for d | +| test.py:124:15:124:15 | ControlFlowNode for l | test.py:128:9:128:9 | ControlFlowNode for l | +| test.py:131:23:131:23 | ControlFlowNode for l | test.py:135:9:135:9 | ControlFlowNode for l | +| test.py:138:15:138:15 | ControlFlowNode for l | test.py:140:9:140:9 | ControlFlowNode for l | +| test.py:145:23:145:23 | ControlFlowNode for l | test.py:147:9:147:9 | ControlFlowNode for l | +nodes +| test.py:2:12:2:12 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:3:5:3:5 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:7:11:7:11 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:8:5:8:5 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:12:14:12:14 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:13:9:13:9 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:17:15:17:15 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:18:5:18:5 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:22:15:22:15 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:23:5:23:5 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:27:12:27:12 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:28:5:28:5 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:38:13:38:13 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:39:5:39:5 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:43:14:43:14 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:44:13:44:13 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:48:14:48:14 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:49:5:49:5 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:53:10:53:10 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:54:5:54:5 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:58:19:58:19 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:59:5:59:5 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:63:28:63:28 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:64:5:64:5 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:67:14:67:14 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:68:5:68:5 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:72:19:72:19 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:73:14:73:14 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:77:17:77:17 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:78:5:78:5 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:82:26:82:26 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:83:5:83:5 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:87:35:87:35 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:88:5:88:5 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:91:21:91:21 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:92:5:92:5 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:96:26:96:26 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:97:21:97:21 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:108:14:108:14 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:109:9:109:9 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:113:20:113:20 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:115:5:115:5 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:119:29:119:29 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:121:5:121:5 | ControlFlowNode for d | semmle.label | ControlFlowNode for d | +| test.py:124:15:124:15 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:128:9:128:9 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:131:23:131:23 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:135:9:135:9 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:138:15:138:15 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:140:9:140:9 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:145:23:145:23 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +| test.py:147:9:147:9 | ControlFlowNode for l | semmle.label | ControlFlowNode for l | +subpaths +#select +| test.py:3:5:3:5 | ControlFlowNode for l | test.py:2:12:2:12 | ControlFlowNode for l | test.py:3:5:3:5 | ControlFlowNode for l | $@ flows to here and is mutated. | test.py:2:12:2:12 | ControlFlowNode for l | Default value | +| test.py:8:5:8:5 | ControlFlowNode for l | test.py:7:11:7:11 | ControlFlowNode for l | test.py:8:5:8:5 | ControlFlowNode for l | $@ flows to here and is mutated. | test.py:7:11:7:11 | ControlFlowNode for l | Default value | +| test.py:13:9:13:9 | ControlFlowNode for l | test.py:12:14:12:14 | ControlFlowNode for l | test.py:13:9:13:9 | ControlFlowNode for l | $@ flows to here and is mutated. | test.py:12:14:12:14 | ControlFlowNode for l | Default value | +| test.py:18:5:18:5 | ControlFlowNode for l | test.py:17:15:17:15 | ControlFlowNode for l | test.py:18:5:18:5 | ControlFlowNode for l | $@ flows to here and is mutated. | test.py:17:15:17:15 | ControlFlowNode for l | Default value | +| test.py:23:5:23:5 | ControlFlowNode for l | test.py:22:15:22:15 | ControlFlowNode for l | test.py:23:5:23:5 | ControlFlowNode for l | $@ flows to here and is mutated. | test.py:22:15:22:15 | ControlFlowNode for l | Default value | +| test.py:28:5:28:5 | ControlFlowNode for l | test.py:27:12:27:12 | ControlFlowNode for l | test.py:28:5:28:5 | ControlFlowNode for l | $@ flows to here and is mutated. | test.py:27:12:27:12 | ControlFlowNode for l | Default value | +| test.py:39:5:39:5 | ControlFlowNode for l | test.py:43:14:43:14 | ControlFlowNode for l | test.py:39:5:39:5 | ControlFlowNode for l | $@ flows to here and is mutated. | test.py:43:14:43:14 | ControlFlowNode for l | Default value | +| test.py:49:5:49:5 | ControlFlowNode for l | test.py:48:14:48:14 | ControlFlowNode for l | test.py:49:5:49:5 | ControlFlowNode for l | $@ flows to here and is mutated. | test.py:48:14:48:14 | ControlFlowNode for l | Default value | +| test.py:54:5:54:5 | ControlFlowNode for d | test.py:53:10:53:10 | ControlFlowNode for d | test.py:54:5:54:5 | ControlFlowNode for d | $@ flows to here and is mutated. | test.py:53:10:53:10 | ControlFlowNode for d | Default value | +| test.py:59:5:59:5 | ControlFlowNode for d | test.py:58:19:58:19 | ControlFlowNode for d | test.py:59:5:59:5 | ControlFlowNode for d | $@ flows to here and is mutated. | test.py:58:19:58:19 | ControlFlowNode for d | Default value | +| test.py:64:5:64:5 | ControlFlowNode for d | test.py:63:28:63:28 | ControlFlowNode for d | test.py:64:5:64:5 | ControlFlowNode for d | $@ flows to here and is mutated. | test.py:63:28:63:28 | ControlFlowNode for d | Default value | +| test.py:68:5:68:5 | ControlFlowNode for d | test.py:72:19:72:19 | ControlFlowNode for d | test.py:68:5:68:5 | ControlFlowNode for d | $@ flows to here and is mutated. | test.py:72:19:72:19 | ControlFlowNode for d | Default value | +| test.py:78:5:78:5 | ControlFlowNode for d | test.py:77:17:77:17 | ControlFlowNode for d | test.py:78:5:78:5 | ControlFlowNode for d | $@ flows to here and is mutated. | test.py:77:17:77:17 | ControlFlowNode for d | Default value | +| test.py:83:5:83:5 | ControlFlowNode for d | test.py:82:26:82:26 | ControlFlowNode for d | test.py:83:5:83:5 | ControlFlowNode for d | $@ flows to here and is mutated. | test.py:82:26:82:26 | ControlFlowNode for d | Default value | +| test.py:88:5:88:5 | ControlFlowNode for d | test.py:87:35:87:35 | ControlFlowNode for d | test.py:88:5:88:5 | ControlFlowNode for d | $@ flows to here and is mutated. | test.py:87:35:87:35 | ControlFlowNode for d | Default value | +| test.py:92:5:92:5 | ControlFlowNode for d | test.py:96:26:96:26 | ControlFlowNode for d | test.py:92:5:92:5 | ControlFlowNode for d | $@ flows to here and is mutated. | test.py:96:26:96:26 | ControlFlowNode for d | Default value | +| test.py:109:9:109:9 | ControlFlowNode for d | test.py:108:14:108:14 | ControlFlowNode for d | test.py:109:9:109:9 | ControlFlowNode for d | $@ flows to here and is mutated. | test.py:108:14:108:14 | ControlFlowNode for d | Default value | +| test.py:115:5:115:5 | ControlFlowNode for d | test.py:113:20:113:20 | ControlFlowNode for d | test.py:115:5:115:5 | ControlFlowNode for d | $@ flows to here and is mutated. | test.py:113:20:113:20 | ControlFlowNode for d | Default value | +| test.py:121:5:121:5 | ControlFlowNode for d | test.py:119:29:119:29 | ControlFlowNode for d | test.py:121:5:121:5 | ControlFlowNode for d | $@ flows to here and is mutated. | test.py:119:29:119:29 | ControlFlowNode for d | Default value | +| test.py:128:9:128:9 | ControlFlowNode for l | test.py:124:15:124:15 | ControlFlowNode for l | test.py:128:9:128:9 | ControlFlowNode for l | $@ flows to here and is mutated. | test.py:124:15:124:15 | ControlFlowNode for l | Default value | +| test.py:135:9:135:9 | ControlFlowNode for l | test.py:131:23:131:23 | ControlFlowNode for l | test.py:135:9:135:9 | ControlFlowNode for l | $@ flows to here and is mutated. | test.py:131:23:131:23 | ControlFlowNode for l | Default value | +| test.py:140:9:140:9 | ControlFlowNode for l | test.py:138:15:138:15 | ControlFlowNode for l | test.py:140:9:140:9 | ControlFlowNode for l | $@ flows to here and is mutated. | test.py:138:15:138:15 | ControlFlowNode for l | Default value | +| test.py:147:9:147:9 | ControlFlowNode for l | test.py:145:23:145:23 | ControlFlowNode for l | test.py:147:9:147:9 | ControlFlowNode for l | $@ flows to here and is mutated. | test.py:145:23:145:23 | ControlFlowNode for l | Default value | diff --git a/python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/ModificationOfParameterWithDefault.qlref b/python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/ModificationOfParameterWithDefault.qlref new file mode 100644 index 000000000000..8c4044e8feeb --- /dev/null +++ b/python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/ModificationOfParameterWithDefault.qlref @@ -0,0 +1 @@ +Functions/ModificationOfParameterWithDefault.ql diff --git a/python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/test.expected b/python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/test.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/test.py b/python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/test.py new file mode 100644 index 000000000000..f1377f36e56e --- /dev/null +++ b/python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/test.py @@ -0,0 +1,150 @@ +# Not OK +def simple(l = [0]): + l[0] = 1 #$ modification=l + return l + +# Not OK +def slice(l = [0]): + l[0:1] = 1 #$ modification=l + return l + +# Not OK +def list_del(l = [0]): + del l[0] #$ modification=l + return l + +# Not OK +def append_op(l = []): + l += [1, 2, 3] #$ modification=l + return l + +# Not OK +def repeat_op(l = [0]): + l *= 3 #$ modification=l + return l + +# Not OK +def append(l = []): + l.append(1) #$ modification=l + return l + +# OK +def includes(l = []): + x = [0] + x.extend(l) + x.extend([1]) + return x + +def extends(l): + l.extend([1]) #$ modification=l + return l + +# Not OK +def deferred(l = []): + extends(l) + return l + +# Not OK +def nonempty(l = [5]): + l.append(1) #$ modification=l + return l + +# Not OK +def dict(d = {}): + d['a'] = 1 #$ modification=d + return d + +# Not OK +def dict_nonempty(d = {'a': 1}): + d['a'] = 2 #$ modification=d + return d + +# OK +def dict_nonempty_nochange(d = {'a': 1}): + d['a'] = 1 #$ SPURIOUS: modification=d + return d + +def modifies(d): + d['a'] = 1 #$ modification=d + return d + +# Not OK +def dict_deferred(d = {}): + modifies(d) + return d + +# Not OK +def dict_method(d = {}): + d.update({'a': 1}) #$ modification=d + return d + +# Not OK +def dict_method_nonempty(d = {'a': 1}): + d.update({'a': 2}) #$ modification=d + return d + +# OK +def dict_method_nonempty_nochange(d = {'a': 1}): + d.update({'a': 1}) #$ SPURIOUS:modification=d + return d + +def modifies_method(d): + d.update({'a': 1}) #$ modification=d + return d + +# Not OK +def dict_deferred_method(d = {}): + modifies_method(d) + return d + +# OK +def dict_includes(d = {}): + x = {} + x.update(d) + x.update({'a': 1}) + return x + +# Not OK +def dict_del(d = {'a': 1}): + del d['a'] #$ modification=d + return d + +# Not OK +def dict_update_op(d = {}): + x = {'a': 1} + d |= x #$ modification=d + return d + +# OK +def dict_update_op_nochange(d = {}): + x = {} + d |= x #$ SPURIOUS: modification=d + return d + +def sanitizer(l = []): + if l: + l.append(1) + else: + l.append(1) #$ modification=l + return l + +def sanitizer_negated(l = [1]): + if not l: + l.append(1) + else: + l.append(1) #$ modification=l + return l + +def sanitizer(l = []): + if not l: + l.append(1) #$ modification=l + else: + l.append(1) + return l + +def sanitizer_negated(l = [1]): + if l: + l.append(1) #$ modification=l + else: + l.append(1) + return l diff --git a/python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/test.ql b/python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/test.ql new file mode 100644 index 000000000000..de516c7ec9ba --- /dev/null +++ b/python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/test.ql @@ -0,0 +1,24 @@ +import python +import semmle.python.dataflow.new.DataFlow +import TestUtilities.InlineExpectationsTest +import semmle.python.functions.ModificationOfParameterWithDefault +private import semmle.python.dataflow.new.internal.PrintNode + +class ModificationOfParameterWithDefaultTest extends InlineExpectationsTest { + ModificationOfParameterWithDefaultTest() { this = "ModificationOfParameterWithDefaultTest" } + + override string getARelevantTag() { result = "modification" } + + predicate relevant_node(DataFlow::Node sink) { + exists(ModificationOfParameterWithDefault::Configuration cfg | cfg.hasFlowTo(sink)) + } + + override predicate hasActualResult(Location location, string element, string tag, string value) { + exists(DataFlow::Node n | relevant_node(n) | + n.getLocation() = location and + tag = "modification" and + value = prettyNode(n) and + element = n.toString() + ) + } +} diff --git a/python/ql/test/query-tests/Functions/general/ModificationOfParameterWithDefault.expected b/python/ql/test/query-tests/Functions/general/ModificationOfParameterWithDefault.expected index 08586d02c100..c32326d49422 100644 --- a/python/ql/test/query-tests/Functions/general/ModificationOfParameterWithDefault.expected +++ b/python/ql/test/query-tests/Functions/general/ModificationOfParameterWithDefault.expected @@ -1,24 +1,44 @@ edges -| functions_test.py:39:9:39:9 | empty mutable value | functions_test.py:40:5:40:5 | empty mutable value | -| functions_test.py:133:15:133:15 | empty mutable value | functions_test.py:134:5:134:5 | empty mutable value | -| functions_test.py:151:25:151:25 | empty mutable value | functions_test.py:152:5:152:5 | empty mutable value | -| functions_test.py:154:21:154:21 | empty mutable value | functions_test.py:155:5:155:5 | empty mutable value | -| functions_test.py:157:27:157:27 | empty mutable value | functions_test.py:158:25:158:25 | empty mutable value | -| functions_test.py:157:27:157:27 | empty mutable value | functions_test.py:159:21:159:21 | empty mutable value | -| functions_test.py:158:25:158:25 | empty mutable value | functions_test.py:151:25:151:25 | empty mutable value | -| functions_test.py:159:21:159:21 | empty mutable value | functions_test.py:154:21:154:21 | empty mutable value | -| functions_test.py:175:28:175:28 | non-empty mutable value | functions_test.py:179:9:179:9 | non-empty mutable value | -| functions_test.py:175:28:175:28 | non-empty mutable value | functions_test.py:181:9:181:9 | non-empty mutable value | -| functions_test.py:188:18:188:18 | non-empty mutable value | functions_test.py:189:28:189:28 | non-empty mutable value | -| functions_test.py:189:28:189:28 | non-empty mutable value | functions_test.py:175:28:175:28 | non-empty mutable value | -| functions_test.py:191:18:191:18 | non-empty mutable value | functions_test.py:192:28:192:28 | non-empty mutable value | -| functions_test.py:192:28:192:28 | non-empty mutable value | functions_test.py:175:28:175:28 | non-empty mutable value | +| functions_test.py:39:9:39:9 | ControlFlowNode for x | functions_test.py:40:5:40:5 | ControlFlowNode for x | +| functions_test.py:133:15:133:15 | ControlFlowNode for x | functions_test.py:134:5:134:5 | ControlFlowNode for x | +| functions_test.py:151:25:151:25 | ControlFlowNode for x | functions_test.py:152:5:152:5 | ControlFlowNode for x | +| functions_test.py:154:21:154:21 | ControlFlowNode for x | functions_test.py:155:5:155:5 | ControlFlowNode for x | +| functions_test.py:157:27:157:27 | ControlFlowNode for y | functions_test.py:158:25:158:25 | ControlFlowNode for y | +| functions_test.py:157:27:157:27 | ControlFlowNode for y | functions_test.py:159:21:159:21 | ControlFlowNode for y | +| functions_test.py:158:25:158:25 | ControlFlowNode for y | functions_test.py:151:25:151:25 | ControlFlowNode for x | +| functions_test.py:159:21:159:21 | ControlFlowNode for y | functions_test.py:154:21:154:21 | ControlFlowNode for x | +| functions_test.py:179:28:179:28 | ControlFlowNode for x | functions_test.py:183:9:183:9 | ControlFlowNode for x | +| functions_test.py:179:28:179:28 | ControlFlowNode for x | functions_test.py:185:9:185:9 | ControlFlowNode for x | +| functions_test.py:192:18:192:18 | ControlFlowNode for x | functions_test.py:193:28:193:28 | ControlFlowNode for x | +| functions_test.py:193:28:193:28 | ControlFlowNode for x | functions_test.py:179:28:179:28 | ControlFlowNode for x | +| functions_test.py:195:18:195:18 | ControlFlowNode for x | functions_test.py:196:28:196:28 | ControlFlowNode for x | +| functions_test.py:196:28:196:28 | ControlFlowNode for x | functions_test.py:179:28:179:28 | ControlFlowNode for x | +nodes +| functions_test.py:39:9:39:9 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +| functions_test.py:40:5:40:5 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +| functions_test.py:133:15:133:15 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +| functions_test.py:134:5:134:5 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +| functions_test.py:151:25:151:25 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +| functions_test.py:152:5:152:5 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +| functions_test.py:154:21:154:21 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +| functions_test.py:155:5:155:5 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +| functions_test.py:157:27:157:27 | ControlFlowNode for y | semmle.label | ControlFlowNode for y | +| functions_test.py:158:25:158:25 | ControlFlowNode for y | semmle.label | ControlFlowNode for y | +| functions_test.py:159:21:159:21 | ControlFlowNode for y | semmle.label | ControlFlowNode for y | +| functions_test.py:179:28:179:28 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +| functions_test.py:183:9:183:9 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +| functions_test.py:185:9:185:9 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +| functions_test.py:192:18:192:18 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +| functions_test.py:193:28:193:28 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +| functions_test.py:195:18:195:18 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +| functions_test.py:196:28:196:28 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | +subpaths #select -| functions_test.py:40:5:40:5 | x | functions_test.py:39:9:39:9 | empty mutable value | functions_test.py:40:5:40:5 | empty mutable value | $@ flows to here and is mutated. | functions_test.py:39:9:39:9 | x | Default value | -| functions_test.py:134:5:134:5 | x | functions_test.py:133:15:133:15 | empty mutable value | functions_test.py:134:5:134:5 | empty mutable value | $@ flows to here and is mutated. | functions_test.py:133:15:133:15 | x | Default value | -| functions_test.py:152:5:152:5 | x | functions_test.py:157:27:157:27 | empty mutable value | functions_test.py:152:5:152:5 | empty mutable value | $@ flows to here and is mutated. | functions_test.py:157:27:157:27 | y | Default value | -| functions_test.py:155:5:155:5 | x | functions_test.py:157:27:157:27 | empty mutable value | functions_test.py:155:5:155:5 | empty mutable value | $@ flows to here and is mutated. | functions_test.py:157:27:157:27 | y | Default value | -| functions_test.py:179:9:179:9 | x | functions_test.py:188:18:188:18 | non-empty mutable value | functions_test.py:179:9:179:9 | non-empty mutable value | $@ flows to here and is mutated. | functions_test.py:188:18:188:18 | x | Default value | -| functions_test.py:179:9:179:9 | x | functions_test.py:191:18:191:18 | non-empty mutable value | functions_test.py:179:9:179:9 | non-empty mutable value | $@ flows to here and is mutated. | functions_test.py:191:18:191:18 | x | Default value | -| functions_test.py:181:9:181:9 | x | functions_test.py:188:18:188:18 | non-empty mutable value | functions_test.py:181:9:181:9 | non-empty mutable value | $@ flows to here and is mutated. | functions_test.py:188:18:188:18 | x | Default value | -| functions_test.py:181:9:181:9 | x | functions_test.py:191:18:191:18 | non-empty mutable value | functions_test.py:181:9:181:9 | non-empty mutable value | $@ flows to here and is mutated. | functions_test.py:191:18:191:18 | x | Default value | +| functions_test.py:40:5:40:5 | ControlFlowNode for x | functions_test.py:39:9:39:9 | ControlFlowNode for x | functions_test.py:40:5:40:5 | ControlFlowNode for x | $@ flows to here and is mutated. | functions_test.py:39:9:39:9 | ControlFlowNode for x | Default value | +| functions_test.py:134:5:134:5 | ControlFlowNode for x | functions_test.py:133:15:133:15 | ControlFlowNode for x | functions_test.py:134:5:134:5 | ControlFlowNode for x | $@ flows to here and is mutated. | functions_test.py:133:15:133:15 | ControlFlowNode for x | Default value | +| functions_test.py:152:5:152:5 | ControlFlowNode for x | functions_test.py:157:27:157:27 | ControlFlowNode for y | functions_test.py:152:5:152:5 | ControlFlowNode for x | $@ flows to here and is mutated. | functions_test.py:157:27:157:27 | ControlFlowNode for y | Default value | +| functions_test.py:155:5:155:5 | ControlFlowNode for x | functions_test.py:157:27:157:27 | ControlFlowNode for y | functions_test.py:155:5:155:5 | ControlFlowNode for x | $@ flows to here and is mutated. | functions_test.py:157:27:157:27 | ControlFlowNode for y | Default value | +| functions_test.py:183:9:183:9 | ControlFlowNode for x | functions_test.py:192:18:192:18 | ControlFlowNode for x | functions_test.py:183:9:183:9 | ControlFlowNode for x | $@ flows to here and is mutated. | functions_test.py:192:18:192:18 | ControlFlowNode for x | Default value | +| functions_test.py:183:9:183:9 | ControlFlowNode for x | functions_test.py:195:18:195:18 | ControlFlowNode for x | functions_test.py:183:9:183:9 | ControlFlowNode for x | $@ flows to here and is mutated. | functions_test.py:195:18:195:18 | ControlFlowNode for x | Default value | +| functions_test.py:185:9:185:9 | ControlFlowNode for x | functions_test.py:192:18:192:18 | ControlFlowNode for x | functions_test.py:185:9:185:9 | ControlFlowNode for x | $@ flows to here and is mutated. | functions_test.py:192:18:192:18 | ControlFlowNode for x | Default value | +| functions_test.py:185:9:185:9 | ControlFlowNode for x | functions_test.py:195:18:195:18 | ControlFlowNode for x | functions_test.py:185:9:185:9 | ControlFlowNode for x | $@ flows to here and is mutated. | functions_test.py:195:18:195:18 | ControlFlowNode for x | Default value | diff --git a/python/ql/test/query-tests/Functions/general/functions_test.py b/python/ql/test/query-tests/Functions/general/functions_test.py index 95e0923ea216..c47794fef28f 100644 --- a/python/ql/test/query-tests/Functions/general/functions_test.py +++ b/python/ql/test/query-tests/Functions/general/functions_test.py @@ -163,11 +163,15 @@ def guarded_modification(z=[]): z.append(0) return z -def issue1143(expr, param=[]): - if not param: - return result - for i in param: - param.remove(i) # Mutation here +# This function causes a discrepancy between the +# Python 2 and 3 versions of the analysis. +# We comment it out until we have resoved the issue. +# +# def issue1143(expr, param=[]): +# if not param: +# return result +# for i in param: +# param.remove(i) # Mutation here # Type guarding of modification of parameter with default: diff --git a/python/ql/test/query-tests/Functions/general/options b/python/ql/test/query-tests/Functions/general/options new file mode 100644 index 000000000000..2032dd9e54c5 --- /dev/null +++ b/python/ql/test/query-tests/Functions/general/options @@ -0,0 +1 @@ +semmle-extractor-options: --max-import-depth=1 --dont-split-graph