diff --git a/python/ql/src/Statements/ModificationOfLocals.ql b/python/ql/src/Statements/ModificationOfLocals.ql index f32ddcf7884..886a49c763e 100644 --- a/python/ql/src/Statements/ModificationOfLocals.ql +++ b/python/ql/src/Statements/ModificationOfLocals.ql @@ -13,9 +13,19 @@ import python private import semmle.python.ApiGraphs +private import semmle.python.dataflow.new.DataFlow predicate originIsLocals(ControlFlowNode n) { - API::builtin("locals").getReturn().getAValueReachableFromSource().asCfgNode() = n + // Only consider the `locals()` dictionary within the scope that called `locals()`. + // Once the dictionary is passed to another scope (e.g. as an argument or via an + // instance attribute) it is just an ordinary mapping, and modifying it is both + // meaningful and effective. Restricting to local (intraprocedural) flow ensures we + // only report modifications in the scope where the `locals()` gotcha actually applies. + exists(DataFlow::LocalSourceNode src, DataFlow::Node use | + src = API::builtin("locals").getReturn().asSource() and + src.flowsTo(use) and + use.asCfgNode() = n + ) } predicate modification_of_locals(ControlFlowNode f) { diff --git a/python/ql/src/change-notes/2026-06-17-modification-of-locals-cross-scope.md b/python/ql/src/change-notes/2026-06-17-modification-of-locals-cross-scope.md new file mode 100644 index 00000000000..5a625a95511 --- /dev/null +++ b/python/ql/src/change-notes/2026-06-17-modification-of-locals-cross-scope.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The `py/modification-of-locals` query no longer flags modifications of a `locals()` dictionary that has been passed out of the scope in which `locals()` was called (for example, by passing it to another function or storing it in an instance attribute). In such cases the dictionary is used as an ordinary mapping and modifying it is meaningful, so these were false positives. The "modification has no effect" claim only applies within the scope that called `locals()`, which is now the only case reported. diff --git a/python/ql/test/query-tests/Statements/general/ModificationOfLocals.expected b/python/ql/test/query-tests/Statements/general/ModificationOfLocals.expected index 5575d3930c1..547cec0c3b8 100644 --- a/python/ql/test/query-tests/Statements/general/ModificationOfLocals.expected +++ b/python/ql/test/query-tests/Statements/general/ModificationOfLocals.expected @@ -3,10 +3,3 @@ | test.py:101:5:101:14 | Attribute() | Modification of the locals() dictionary will have no effect on the local variables. | | test.py:102:9:102:14 | Subscript | Modification of the locals() dictionary will have no effect on the local variables. | | test.py:103:5:103:13 | Attribute() | Modification of the locals() dictionary will have no effect on the local variables. | -| test.py:206:5:206:11 | Subscript | Modification of the locals() dictionary will have no effect on the local variables. | -| test.py:207:5:207:23 | Attribute() | Modification of the locals() dictionary will have no effect on the local variables. | -| test.py:208:5:208:15 | Attribute() | Modification of the locals() dictionary will have no effect on the local variables. | -| test.py:209:9:209:15 | Subscript | Modification of the locals() dictionary will have no effect on the local variables. | -| test.py:210:5:210:14 | Attribute() | Modification of the locals() dictionary will have no effect on the local variables. | -| test.py:228:9:228:24 | Subscript | Modification of the locals() dictionary will have no effect on the local variables. | -| test.py:229:9:229:35 | Attribute() | Modification of the locals() dictionary will have no effect on the local variables. |