github / github/codeql

[Python] False Negative: Taint flow fails to penetrate function-local class definitions

Ouverte
#20,947 4 commentaires 0 réactions 0 personnes assignées Voir sur GitHub
question
Langage dominant
CodeQL
Étoiles
10.1k
Forks
2.1k
Merge moyen
2 j 15 h
PR mergées (30 j)
141

Description

### Description

I have identified a false negative in Python DataFlow analysis where taint tracking is lost when a class is defined **inside a function**.

If a tainted variable is passed as an argument to a function, and that argument is subsequently used inside a class defined within that function (a function-local class), CodeQL fails to track the data flow to the class instance's attributes.

However, if a similar logic is applied using a top-level (module-level) class, the data flow is detected correctly. This suggests an issue with how data flow is handled across the scope boundary of locally defined classes.

### Reproduction Case (False Negative)

In this example, `taint_src` is passed to `constructor_field_001_T`. The class `A` is defined inside the function and captures `taint_src`. The flow to `os.system` is **NOT detected**.

```python
import os

def constructor_field_001_T(taint_src):
# Class defined inside the function scope
class A:
def __init__(self):
# ISSUE: The analyzer fails to track 'taint_src' from the
# outer function argument into this local class scope.
self.data = taint_src
self.sani = '_'

obj = A()
taint_sink(obj.data)

def taint_sink(o):
os.system(o)

if __name__ == "__main__":
taint_src = "taint_src_value"
constructor_field_001_T(taint_src)
```

### Control Case (Working)

In this example, the class `A` is defined at the module level. The flow to `os.system` **IS detected** correctly.

```python
import os

# Class defined at module level
class A:
def __init__(self):
# Accessing taint_src (as a global/captured in this context) works fine
self.data = taint_src
self.sani = '_'

def constructor_field_001_T(taint_src):
obj = A()
taint_sink(obj.data)

def taint_sink(o):
os.system(o)

if __name__ == "__main__":
taint_src = "taint_src_value"
constructor_field_001_T(taint_src)
```

### Additional Control Case (Working: Explicit Argument)

Significantly, if I keep the class inside the function but pass `taint_src` as an **explicit argument** to `__init__`, the flow **IS detected**.
```python
import os

def constructor_field_explicit_arg(taint_src):
# Class defined inside function
class A:
# Explicit argument instead of capture
def __init__(self, val):
self.data = val

# Passing taint explicitly
obj = A(taint_src)
taint_sink(obj.data)

def taint_sink(o):
os.system(o)

if __name__ == "__main__":
taint_src = "taint_src_value"
constructor_field_explicit_arg(taint_src)
```

### CodeQL Query Used

I am using a standard `DataFlow::Global` configuration looking for the specific string literal flowing to `os.system`.

Click to view query

```ql
/**
* @name Python Taint Reproduction
* @kind path-problem
* @problem.severity error
* @id py/taint-reproduction
*/

import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.TaintTracking

class TaintSource extends DataFlow::Node {
TaintSource() {
exists(StrConst str |
str.getText() = "taint_src_value" and
this.asExpr() = str
)
}
}

class DangerousSink extends DataFlow::Node {
DangerousSink() {
exists(Call call |
(
call.getFunc().(Attribute).getName() = "system" and
call.getFunc().(Attribute).getObject().(Name).getId() = "os"
) and
this.asExpr() = call.getAnArg()
)
}
}

module TaintConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof TaintSource
}

predicate isSink(DataFlow::Node sink) {
sink instanceof DangerousSink
}
}

module TaintFlow = TaintTracking::Global;
import TaintFlow::PathGraph

from TaintFlow::PathNode source, TaintFlow::PathNode sink
where TaintFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "Taint flow detected"
```

### Expected Behavior

CodeQL should be able to track the `taint_src` argument into the `__init__` method of the locally defined class `A`, eventually leading to the `os.system` sink, just as it does for top-level classes.

Guide de contribution

Ouvrir le guide de contribution

Piste de recherche

Start by running the supplied Python reproduction with the standard DataFlow::Global configuration and compare the local-class case with the top-level and explicit-argument controls. Read the imported semmle.python.dataflow.new.DataFlow and TaintTracking libraries; done means the query reports a flow from "taint_src_value" through the local class attribute to os.system.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
python
Domaine
security
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
À l'abandon
Clarté
Plutôt claire
Accessibilité débutants
45/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.