From ebf09cb304970ac20ae9000d31b913d2b17ad305 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:04:11 +0000 Subject: [PATCH 1/4] Initial plan From 1d1594e20c7ebaac30130e73f37b96e30967d938 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:33:44 +0000 Subject: [PATCH 2/4] Add taint flow summaries for list.extend and list.insert Co-authored-by: hvitved <3667920+hvitved@users.noreply.github.com> --- .../2026-08-10-list-extend-insert-taint.md | 4 ++ .../lib/semmle/python/frameworks/Stdlib.qll | 64 +++++++++++++++++++ .../dataflow/coverage/test_builtins.py | 10 +++ .../test_collections.py | 34 ++++++++++ 4 files changed, 112 insertions(+) create mode 100644 python/ql/lib/change-notes/2026-08-10-list-extend-insert-taint.md 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() From 62fdb69be19675b89cd61af39c46cbf9565231ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 07:26:27 +0000 Subject: [PATCH 3/4] Remove redundant blunt taint fallback steps from ListExtend and ListInsert Co-authored-by: hvitved <3667920+hvitved@users.noreply.github.com> --- python/ql/lib/semmle/python/frameworks/Stdlib.qll | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/python/ql/lib/semmle/python/frameworks/Stdlib.qll b/python/ql/lib/semmle/python/frameworks/Stdlib.qll index 327b4a2629e5..b9ad7bf63702 100644 --- a/python/ql/lib/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/lib/semmle/python/frameworks/Stdlib.qll @@ -4906,11 +4906,6 @@ module StdlibPrivate { ) 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 } } @@ -4935,11 +4930,6 @@ module StdlibPrivate { 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 } } From 6846a3721721553fab00a520d3deca3e77dfeeea Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Tue, 11 Aug 2026 11:17:55 +0200 Subject: [PATCH 4/4] Python: Update expected test output --- .../ql/test/library-tests/dataflow/regression/dataflow.expected | 1 + python/ql/test/library-tests/dataflow/regression/test.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/python/ql/test/library-tests/dataflow/regression/dataflow.expected b/python/ql/test/library-tests/dataflow/regression/dataflow.expected index c8ffed514463..ea2d5a93bdfe 100644 --- a/python/ql/test/library-tests/dataflow/regression/dataflow.expected +++ b/python/ql/test/library-tests/dataflow/regression/dataflow.expected @@ -15,6 +15,7 @@ | test.py:128:13:128:18 | ControlFlowNode for SOURCE | test.py:132:14:132:14 | ControlFlowNode for t | | test.py:159:10:159:15 | ControlFlowNode for SOURCE | test.py:160:14:160:14 | ControlFlowNode for t | | test.py:163:9:163:14 | ControlFlowNode for SOURCE | test.py:165:12:165:12 | ControlFlowNode for s | +| test.py:168:10:168:15 | ControlFlowNode for SOURCE | test.py:172:10:172:13 | ControlFlowNode for Subscript | | test.py:178:9:178:14 | ControlFlowNode for SOURCE | test.py:180:14:180:14 | ControlFlowNode for t | | test.py:178:9:178:14 | ControlFlowNode for SOURCE | test.py:182:16:182:16 | ControlFlowNode for t | | test.py:178:9:178:14 | ControlFlowNode for SOURCE | test.py:184:16:184:16 | ControlFlowNode for t | diff --git a/python/ql/test/library-tests/dataflow/regression/test.py b/python/ql/test/library-tests/dataflow/regression/test.py index 869993a5b643..de08437c3f2f 100644 --- a/python/ql/test/library-tests/dataflow/regression/test.py +++ b/python/ql/test/library-tests/dataflow/regression/test.py @@ -169,7 +169,7 @@ def test_update_extend(x, y): d = {"key" : SOURCE} x.extend(l) y.update(d) - SINK(x[0]) # Flow not found + SINK(x[0]) SINK(y["key"]) # Flow not found l2 = list(l) d2 = dict(d)