diff --git a/python/ql/lib/change-notes/2026-08-10-list-extend-insert-taint.md b/python/ql/lib/change-notes/2026-08-10-list-extend-insert-taint.md new file mode 100644 index 000000000000..db362bde20f6 --- /dev/null +++ b/python/ql/lib/change-notes/2026-08-10-list-extend-insert-taint.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Added taint flow through `list.extend` and `list.insert`, matching the existing taint flow through `list.append`. diff --git a/python/ql/lib/semmle/python/frameworks/Stdlib.qll b/python/ql/lib/semmle/python/frameworks/Stdlib.qll index df69a010fd8b..327b4a2629e5 100644 --- a/python/ql/lib/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/lib/semmle/python/frameworks/Stdlib.qll @@ -4879,6 +4879,70 @@ module StdlibPrivate { } } + /** + * A flow summary for `list.extend`. + * + * See https://docs.python.org/3.10/library/stdtypes.html#typesseq-mutable + */ + class ListExtend extends SummarizedCallable::Range { + ListExtend() { this = "list.extend" } + + override DataFlow::CallCfgNode getACall() { + result.(DataFlow::MethodCallNode).calls(_, "extend") + } + + override DataFlow::ArgumentNode getACallback() { + result.(DataFlow::AttrRead).getAttributeName() = "extend" + } + + override predicate propagatesFlow(string input, string output, boolean preservesValue) { + // elements of the newly added iterable are added to this + ( + input = "Argument[0].ListElement" + or + input = "Argument[0].SetElement" + or + input = "Argument[0].AnyTupleElement" + ) and + output = "Argument[self].ListElement" and + preservesValue = true + or + // transfer taint from new iterable to this (TODO: remove in future when taint-handling is more in line with other languages) + input = "Argument[0]" and + output = "Argument[self]" and + preservesValue = false + } + } + + /** + * A flow summary for `list.insert`. + * + * See https://docs.python.org/3.10/library/stdtypes.html#typesseq-mutable + */ + class ListInsert extends SummarizedCallable::Range { + ListInsert() { this = "list.insert" } + + override DataFlow::CallCfgNode getACall() { + result.(DataFlow::MethodCallNode).calls(_, "insert") + } + + override DataFlow::ArgumentNode getACallback() { + result.(DataFlow::AttrRead).getAttributeName() = "insert" + } + + override predicate propagatesFlow(string input, string output, boolean preservesValue) { + // newly added element added to this + input = "Argument[1]" and + output = "Argument[self].ListElement" and + preservesValue = true + or + // transfer taint from new element to this (TODO: remove in future when taint-handling is more in line with other languages) + input = "Argument[1]" and + output = "Argument[self]" and + preservesValue = false + } + } + /** * A flow summary for `set.add`. * diff --git a/python/ql/test/library-tests/dataflow/coverage/test_builtins.py b/python/ql/test/library-tests/dataflow/coverage/test_builtins.py index 7ef7866ec175..bff447335449 100644 --- a/python/ql/test/library-tests/dataflow/coverage/test_builtins.py +++ b/python/ql/test/library-tests/dataflow/coverage/test_builtins.py @@ -181,6 +181,16 @@ def test_list_append(): l.append(SOURCE) SINK(l[1]) # $ flow="SOURCE, l:-1 -> l[1]" +def test_list_extend(): + l = [NONSOURCE] + l.extend([SOURCE]) + SINK(l[1]) # $ flow="SOURCE, l:-1 -> l[1]" + +def test_list_insert(): + l = [NONSOURCE] + l.insert(0, SOURCE) + SINK(l[0]) # $ flow="SOURCE, l:-1 -> l[0]" + ### Set def test_set_pop(): diff --git a/python/ql/test/library-tests/dataflow/tainttracking/defaultAdditionalTaintStep/test_collections.py b/python/ql/test/library-tests/dataflow/tainttracking/defaultAdditionalTaintStep/test_collections.py index fa6087f3ebcd..f738e14af8d4 100644 --- a/python/ql/test/library-tests/dataflow/tainttracking/defaultAdditionalTaintStep/test_collections.py +++ b/python/ql/test/library-tests/dataflow/tainttracking/defaultAdditionalTaintStep/test_collections.py @@ -232,6 +232,37 @@ def list_extend(): ensure_not_tainted(my_list) my_list.extend(tainted_list) + ensure_tainted(my_list) # $ tainted + + +def list_extend_iteration(): + my_list = ["safe"] + tainted_list = [TAINTED_STRING] + + ensure_not_tainted(my_list) + + my_list.extend(tainted_list) + for x in my_list: + ensure_tainted(x) # $ tainted + + +def list_insert(): + tainted_string = TAINTED_STRING + my_list = ["safe"] + + ensure_not_tainted(my_list) + + my_list.insert(0, tainted_string) + ensure_tainted(my_list) # $ tainted + + +def list_iadd(): + my_list = ["safe"] + tainted_list = [TAINTED_STRING] + + ensure_not_tainted(my_list) + + my_list += tainted_list ensure_tainted(my_list) # $ MISSING: tainted @@ -308,6 +339,9 @@ def set_add(): list_index_aug_assign() list_append() list_extend() +list_extend_iteration() +list_insert() +list_iadd() dict_update_dict() dict_update_kv_list()