Merge pull request #20162 from Napalys/python/global_variable_tracking

Python: Add jump steps for global variable nested field access
This commit is contained in:
Napalys Klicius
2025-09-17 13:38:07 +02:00
committed by GitHub
14 changed files with 226 additions and 59 deletions

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Data flow tracking through global variables now supports nested field access patterns such as `global_var.obj.field`. This improves the precision of taint tracking analysis when data flows through complex global variable structures.

View File

@@ -64,6 +64,15 @@ abstract class AttrRef extends Node {
abstract class AttrWrite extends AttrRef {
/** Gets the data flow node corresponding to the value that is written to the attribute. */
abstract Node getValue();
/**
* Holds if this attribute write writes the attribute named `attrName` on object `object` with
* value `value`.
*/
predicate writes(Node object, string attrName, Node value) {
this.accesses(object, attrName) and
this.getValue() = value
}
}
/**
@@ -225,7 +234,10 @@ private class ClassDefinitionAsAttrWrite extends AttrWrite, CfgNode {
* - Dynamic attribute reads using `getattr`: `getattr(object, attr)`
* - Qualified imports: `from module import attr as name`
*/
abstract class AttrRead extends AttrRef, Node, LocalSourceNode { }
abstract class AttrRead extends AttrRef, Node, LocalSourceNode {
/** Holds if this attribute read reads the attribute named `attrName` on the object `object`. */
predicate reads(Node object, string attrName) { this.accesses(object, attrName) }
}
/** A simple attribute read, e.g. `object.attr` */
private class AttributeReadAsAttrRead extends AttrRead, CfgNode {

View File

@@ -556,6 +556,75 @@ predicate runtimeJumpStep(Node nodeFrom, Node nodeTo) {
nodeFrom.asCfgNode() = param.getDefault() and
nodeTo.asCfgNode() = param.getDefiningNode()
)
or
// Enhanced global variable field access tracking
globalVariableNestedFieldJumpStep(nodeFrom, nodeTo)
}
/** Helper predicate for `globalVariableNestedFieldJumpStep`. */
pragma[nomagic]
private predicate globalVariableAttrPathRead(
ModuleVariableNode globalVar, string accessPath, AttrRead r, string attrName
) {
globalVariableAttrPathAtDepth(globalVar, accessPath, r.getObject(), _) and
attrName = r.getAttributeName()
}
/** Helper predicate for `globalVariableNestedFieldJumpStep`. */
pragma[nomagic]
private predicate globalVariableAttrPathWrite(
ModuleVariableNode globalVar, string accessPath, AttrWrite w, string attrName
) {
globalVariableAttrPathAtDepth(globalVar, accessPath, w.getObject(), _) and
attrName = w.getAttributeName()
}
/**
* Holds if there is a jump step from `nodeFrom` to `nodeTo` through global variable field access.
* This supports tracking nested object field access through global variables like `app.obj.foo`.
*/
pragma[nomagic]
private predicate globalVariableNestedFieldJumpStep(Node nodeFrom, Node nodeTo) {
exists(ModuleVariableNode globalVar, AttrWrite write, AttrRead read |
// Match writes and reads on the same global variable attribute path
exists(string accessPath, string attrName |
globalVariableAttrPathRead(globalVar, accessPath, read, attrName) and
globalVariableAttrPathWrite(globalVar, accessPath, write, attrName)
) and
nodeFrom = write.getValue() and
nodeTo = read
)
}
/**
* Maximum depth for global variable nested attribute access.
* Depth 1 = globalVar.foo, depth 2 = globalVar.foo.bar, depth 3 = globalVar.foo.bar.baz, etc.
*/
private int getMaxGlobalVariableDepth() { result = 2 }
/**
* Holds if `node` is an attribute access path starting from global variable `globalVar` at specific `depth`.
*/
private predicate globalVariableAttrPathAtDepth(
ModuleVariableNode globalVar, string accessPath, Node node, int depth
) {
// Base case: Direct global variable access (depth 0)
depth = 0 and
// We use `globalVar` instead of `globalVar.getAWrite()` due to some weirdness with how
// attribute writes are handled in the global scope (see `GlobalAttributeAssignmentAsAttrWrite`).
node in [globalVar.getARead(), globalVar] and
accessPath = ""
or
exists(Node obj, string attrName, string parentAccessPath, int parentDepth |
node.(AttrRead).reads(obj, attrName)
or
any(AttrWrite aw).writes(obj, attrName, node)
|
globalVariableAttrPathAtDepth(globalVar, parentAccessPath, obj, parentDepth) and
accessPath = parentAccessPath + "." + attrName and
depth = parentDepth + 1 and
depth <= getMaxGlobalVariableDepth()
)
}
//--------

View File

@@ -301,12 +301,12 @@ def set_to_source():
@expects(4) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
def test_global_flow_to_class_attribute():
inst = WithTuple2()
SINK_F(WithTuple2.my_tuple[0])
SINK_F(WithTuple2.my_tuple[0]) # $ SPURIOUS: flow="SOURCE, l:-5 -> WithTuple2.my_tuple[0]"
SINK_F(inst.my_tuple[0])
set_to_source()
SINK(WithTuple2.my_tuple[0]) # $ MISSING: flow="SOURCE, l:-10 -> WithTuple2.my_tuple[0]"
SINK(WithTuple2.my_tuple[0]) # $ flow="SOURCE, l:-10 -> WithTuple2.my_tuple[0]"
SINK(inst.my_tuple[0]) # $ MISSING: flow="SOURCE, l:-11 -> inst.my_tuple[0]"

View File

@@ -146,6 +146,7 @@ SINK(fields_with_local_flow(SOURCE)) # $ flow="SOURCE -> fields_with_local_flow(
class NestedObj(object):
def __init__(self):
self.obj = MyObj("OK")
self.obj.foo = "default"
def getObj(self):
return self.obj
@@ -163,17 +164,31 @@ x2 = SOURCE
a2 = NestedObj()
a2.getObj().foo = x2
SINK(a2.obj.foo) # $ flow="SOURCE, l:-3 -> a2.obj.foo"
# Global variable
app = NestedObj()
def init_global():
app.obj.foo = SOURCE
def read_global():
return app.obj.foo
def test_global_nested_attributes():
init_global()
result = read_global()
SINK(result) # $ flow="SOURCE, l:-8 -> result"
# ------------------------------------------------------------------------------
# Global scope interaction
# ------------------------------------------------------------------------------
def func_defined_before():
SINK(global_obj.foo) # $ MISSING: flow="SOURCE, l:+3 -> global_obj.foo"
SINK(global_obj.foo) # $ flow="SOURCE, l:+3 -> global_obj.foo"
global_obj = MyObj(NONSOURCE)
global_obj.foo = SOURCE
SINK(global_obj.foo) # $ flow="SOURCE, l:-1 -> global_obj.foo"
def func_defined_after():
SINK(global_obj.foo) # $ MISSING: flow="SOURCE, l:-4 -> global_obj.foo"
SINK(global_obj.foo) # $ flow="SOURCE, l:-4 -> global_obj.foo"

View File

@@ -1,2 +0,0 @@
missingAnnotationOnSink
testFailures

View File

@@ -1,4 +0,0 @@
import python
import utils.test.dataflow.DataflowQueryTest
import semmle.python.security.dataflow.PathInjectionQuery
import FromTaintTrackingStateConfig<PathInjectionConfig>

View File

@@ -1,4 +1,38 @@
#select
| fastapi_path_injection.py:7:19:7:26 | ControlFlowNode for filepath | fastapi_path_injection.py:17:21:17:24 | ControlFlowNode for path | fastapi_path_injection.py:7:19:7:26 | ControlFlowNode for filepath | This path depends on a $@. | fastapi_path_injection.py:17:21:17:24 | ControlFlowNode for path | user-provided value |
| fastapi_path_injection.py:7:19:7:26 | ControlFlowNode for filepath | fastapi_path_injection.py:26:21:26:24 | ControlFlowNode for path | fastapi_path_injection.py:7:19:7:26 | ControlFlowNode for filepath | This path depends on a $@. | fastapi_path_injection.py:26:21:26:24 | ControlFlowNode for path | user-provided value |
| fastapi_path_injection.py:7:19:7:26 | ControlFlowNode for filepath | fastapi_path_injection.py:31:21:31:24 | ControlFlowNode for path | fastapi_path_injection.py:7:19:7:26 | ControlFlowNode for filepath | This path depends on a $@. | fastapi_path_injection.py:31:21:31:24 | ControlFlowNode for path | user-provided value |
| fastapi_path_injection.py:7:19:7:26 | ControlFlowNode for filepath | fastapi_path_injection.py:48:21:48:24 | ControlFlowNode for path | fastapi_path_injection.py:7:19:7:26 | ControlFlowNode for filepath | This path depends on a $@. | fastapi_path_injection.py:48:21:48:24 | ControlFlowNode for path | user-provided value |
| flask_path_injection.py:21:32:21:38 | ControlFlowNode for dirname | flask_path_injection.py:1:26:1:32 | ControlFlowNode for ImportMember | flask_path_injection.py:21:32:21:38 | ControlFlowNode for dirname | This path depends on a $@. | flask_path_injection.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:13:14:13:47 | ControlFlowNode for Attribute() | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:13:14:13:47 | ControlFlowNode for Attribute() | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:21:14:21:18 | ControlFlowNode for npath | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:21:14:21:18 | ControlFlowNode for npath | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:31:14:31:18 | ControlFlowNode for npath | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:31:14:31:18 | ControlFlowNode for npath | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:48:14:48:18 | ControlFlowNode for npath | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:48:14:48:18 | ControlFlowNode for npath | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:65:14:65:18 | ControlFlowNode for npath | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:65:14:65:18 | ControlFlowNode for npath | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:87:18:87:37 | ControlFlowNode for possibly_unsafe_path | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:87:18:87:37 | ControlFlowNode for possibly_unsafe_path | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:94:14:94:17 | ControlFlowNode for path | path_injection.py:91:20:91:25 | ControlFlowNode for foo_id | path_injection.py:94:14:94:17 | ControlFlowNode for path | This path depends on a $@. | path_injection.py:91:20:91:25 | ControlFlowNode for foo_id | user-provided value |
| path_injection.py:102:14:102:17 | ControlFlowNode for path | path_injection.py:98:20:98:22 | ControlFlowNode for foo | path_injection.py:102:14:102:17 | ControlFlowNode for path | This path depends on a $@. | path_injection.py:98:20:98:22 | ControlFlowNode for foo | user-provided value |
| path_injection.py:113:14:113:17 | ControlFlowNode for path | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:113:14:113:17 | ControlFlowNode for path | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:124:14:124:17 | ControlFlowNode for path | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:124:14:124:17 | ControlFlowNode for path | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:132:14:132:22 | ControlFlowNode for sanitized | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:132:14:132:22 | ControlFlowNode for sanitized | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:142:14:142:17 | ControlFlowNode for path | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:142:14:142:17 | ControlFlowNode for path | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:152:18:152:21 | ControlFlowNode for path | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:152:18:152:21 | ControlFlowNode for path | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| pathlib_use.py:14:5:14:5 | ControlFlowNode for p | pathlib_use.py:3:26:3:32 | ControlFlowNode for ImportMember | pathlib_use.py:14:5:14:5 | ControlFlowNode for p | This path depends on a $@. | pathlib_use.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| pathlib_use.py:17:5:17:6 | ControlFlowNode for p2 | pathlib_use.py:3:26:3:32 | ControlFlowNode for ImportMember | pathlib_use.py:17:5:17:6 | ControlFlowNode for p2 | This path depends on a $@. | pathlib_use.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| test.py:19:10:19:10 | ControlFlowNode for x | test.py:3:26:3:32 | ControlFlowNode for ImportMember | test.py:19:10:19:10 | ControlFlowNode for x | This path depends on a $@. | test.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| test.py:26:10:26:10 | ControlFlowNode for y | test.py:3:26:3:32 | ControlFlowNode for ImportMember | test.py:26:10:26:10 | ControlFlowNode for y | This path depends on a $@. | test.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| test.py:33:14:33:14 | ControlFlowNode for x | test.py:3:26:3:32 | ControlFlowNode for ImportMember | test.py:33:14:33:14 | ControlFlowNode for x | This path depends on a $@. | test.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| test.py:49:14:49:14 | ControlFlowNode for y | test.py:3:26:3:32 | ControlFlowNode for ImportMember | test.py:49:14:49:14 | ControlFlowNode for y | This path depends on a $@. | test.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
edges
| fastapi_path_injection.py:6:24:6:31 | ControlFlowNode for filepath | fastapi_path_injection.py:7:19:7:26 | ControlFlowNode for filepath | provenance | |
| fastapi_path_injection.py:17:21:17:24 | ControlFlowNode for path | fastapi_path_injection.py:20:34:20:37 | ControlFlowNode for path | provenance | |
| fastapi_path_injection.py:20:34:20:37 | ControlFlowNode for path | fastapi_path_injection.py:6:24:6:31 | ControlFlowNode for filepath | provenance | |
| fastapi_path_injection.py:26:21:26:24 | ControlFlowNode for path | fastapi_path_injection.py:27:34:27:37 | ControlFlowNode for path | provenance | |
| fastapi_path_injection.py:27:34:27:37 | ControlFlowNode for path | fastapi_path_injection.py:6:24:6:31 | ControlFlowNode for filepath | provenance | |
| fastapi_path_injection.py:31:21:31:24 | ControlFlowNode for path | fastapi_path_injection.py:32:34:32:37 | ControlFlowNode for path | provenance | |
| fastapi_path_injection.py:32:34:32:37 | ControlFlowNode for path | fastapi_path_injection.py:6:24:6:31 | ControlFlowNode for filepath | provenance | |
| fastapi_path_injection.py:48:21:48:24 | ControlFlowNode for path | fastapi_path_injection.py:49:45:49:48 | ControlFlowNode for path | provenance | |
| fastapi_path_injection.py:49:45:49:48 | ControlFlowNode for path | fastapi_path_injection.py:6:24:6:31 | ControlFlowNode for filepath | provenance | |
| flask_path_injection.py:1:26:1:32 | ControlFlowNode for ImportMember | flask_path_injection.py:1:26:1:32 | ControlFlowNode for request | provenance | |
| flask_path_injection.py:1:26:1:32 | ControlFlowNode for request | flask_path_injection.py:19:15:19:21 | ControlFlowNode for request | provenance | |
| flask_path_injection.py:19:5:19:11 | ControlFlowNode for dirname | flask_path_injection.py:21:32:21:38 | ControlFlowNode for dirname | provenance | |
@@ -122,6 +156,16 @@ edges
| test.py:48:23:48:23 | ControlFlowNode for x | test.py:12:15:12:15 | ControlFlowNode for x | provenance | |
| test.py:48:23:48:23 | ControlFlowNode for x | test.py:48:13:48:24 | ControlFlowNode for normalize() | provenance | Config |
nodes
| fastapi_path_injection.py:6:24:6:31 | ControlFlowNode for filepath | semmle.label | ControlFlowNode for filepath |
| fastapi_path_injection.py:7:19:7:26 | ControlFlowNode for filepath | semmle.label | ControlFlowNode for filepath |
| fastapi_path_injection.py:17:21:17:24 | ControlFlowNode for path | semmle.label | ControlFlowNode for path |
| fastapi_path_injection.py:20:34:20:37 | ControlFlowNode for path | semmle.label | ControlFlowNode for path |
| fastapi_path_injection.py:26:21:26:24 | ControlFlowNode for path | semmle.label | ControlFlowNode for path |
| fastapi_path_injection.py:27:34:27:37 | ControlFlowNode for path | semmle.label | ControlFlowNode for path |
| fastapi_path_injection.py:31:21:31:24 | ControlFlowNode for path | semmle.label | ControlFlowNode for path |
| fastapi_path_injection.py:32:34:32:37 | ControlFlowNode for path | semmle.label | ControlFlowNode for path |
| fastapi_path_injection.py:48:21:48:24 | ControlFlowNode for path | semmle.label | ControlFlowNode for path |
| fastapi_path_injection.py:49:45:49:48 | ControlFlowNode for path | semmle.label | ControlFlowNode for path |
| flask_path_injection.py:1:26:1:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
| flask_path_injection.py:1:26:1:32 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
| flask_path_injection.py:19:5:19:11 | ControlFlowNode for dirname | semmle.label | ControlFlowNode for dirname |
@@ -252,24 +296,3 @@ nodes
subpaths
| test.py:25:19:25:19 | ControlFlowNode for x | test.py:12:15:12:15 | ControlFlowNode for x | test.py:13:12:13:30 | ControlFlowNode for Attribute() | test.py:25:9:25:20 | ControlFlowNode for normalize() |
| test.py:48:23:48:23 | ControlFlowNode for x | test.py:12:15:12:15 | ControlFlowNode for x | test.py:13:12:13:30 | ControlFlowNode for Attribute() | test.py:48:13:48:24 | ControlFlowNode for normalize() |
#select
| flask_path_injection.py:21:32:21:38 | ControlFlowNode for dirname | flask_path_injection.py:1:26:1:32 | ControlFlowNode for ImportMember | flask_path_injection.py:21:32:21:38 | ControlFlowNode for dirname | This path depends on a $@. | flask_path_injection.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:13:14:13:47 | ControlFlowNode for Attribute() | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:13:14:13:47 | ControlFlowNode for Attribute() | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:21:14:21:18 | ControlFlowNode for npath | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:21:14:21:18 | ControlFlowNode for npath | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:31:14:31:18 | ControlFlowNode for npath | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:31:14:31:18 | ControlFlowNode for npath | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:48:14:48:18 | ControlFlowNode for npath | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:48:14:48:18 | ControlFlowNode for npath | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:65:14:65:18 | ControlFlowNode for npath | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:65:14:65:18 | ControlFlowNode for npath | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:87:18:87:37 | ControlFlowNode for possibly_unsafe_path | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:87:18:87:37 | ControlFlowNode for possibly_unsafe_path | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:94:14:94:17 | ControlFlowNode for path | path_injection.py:91:20:91:25 | ControlFlowNode for foo_id | path_injection.py:94:14:94:17 | ControlFlowNode for path | This path depends on a $@. | path_injection.py:91:20:91:25 | ControlFlowNode for foo_id | user-provided value |
| path_injection.py:102:14:102:17 | ControlFlowNode for path | path_injection.py:98:20:98:22 | ControlFlowNode for foo | path_injection.py:102:14:102:17 | ControlFlowNode for path | This path depends on a $@. | path_injection.py:98:20:98:22 | ControlFlowNode for foo | user-provided value |
| path_injection.py:113:14:113:17 | ControlFlowNode for path | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:113:14:113:17 | ControlFlowNode for path | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:124:14:124:17 | ControlFlowNode for path | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:124:14:124:17 | ControlFlowNode for path | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:132:14:132:22 | ControlFlowNode for sanitized | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:132:14:132:22 | ControlFlowNode for sanitized | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:142:14:142:17 | ControlFlowNode for path | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:142:14:142:17 | ControlFlowNode for path | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| path_injection.py:152:18:152:21 | ControlFlowNode for path | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | path_injection.py:152:18:152:21 | ControlFlowNode for path | This path depends on a $@. | path_injection.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| pathlib_use.py:14:5:14:5 | ControlFlowNode for p | pathlib_use.py:3:26:3:32 | ControlFlowNode for ImportMember | pathlib_use.py:14:5:14:5 | ControlFlowNode for p | This path depends on a $@. | pathlib_use.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| pathlib_use.py:17:5:17:6 | ControlFlowNode for p2 | pathlib_use.py:3:26:3:32 | ControlFlowNode for ImportMember | pathlib_use.py:17:5:17:6 | ControlFlowNode for p2 | This path depends on a $@. | pathlib_use.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| test.py:19:10:19:10 | ControlFlowNode for x | test.py:3:26:3:32 | ControlFlowNode for ImportMember | test.py:19:10:19:10 | ControlFlowNode for x | This path depends on a $@. | test.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| test.py:26:10:26:10 | ControlFlowNode for y | test.py:3:26:3:32 | ControlFlowNode for ImportMember | test.py:26:10:26:10 | ControlFlowNode for y | This path depends on a $@. | test.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| test.py:33:14:33:14 | ControlFlowNode for x | test.py:3:26:3:32 | ControlFlowNode for ImportMember | test.py:33:14:33:14 | ControlFlowNode for x | This path depends on a $@. | test.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |
| test.py:49:14:49:14 | ControlFlowNode for y | test.py:3:26:3:32 | ControlFlowNode for ImportMember | test.py:49:14:49:14 | ControlFlowNode for y | This path depends on a $@. | test.py:3:26:3:32 | ControlFlowNode for ImportMember | user-provided value |

View File

@@ -1 +1,2 @@
Security/CWE-022/PathInjection.ql
query: Security/CWE-022/PathInjection.ql
postprocess: utils/test/InlineExpectationsTestQuery.ql

View File

@@ -0,0 +1,49 @@
from fastapi import FastAPI, Depends
app = FastAPI()
class FileHandler:
def get_data(self, filepath: str):
with open(filepath, "r") as f: # $ Alert
return f.readline()
file_handler = None
def init_file_handler():
global file_handler
file_handler = FileHandler()
@app.get("/file/")
async def read_item(path: str): # $ Source
if file_handler is None:
init_file_handler()
return file_handler.get_data(path)
def init_file_handler():
return FileHandler()
@app.get("/file2/", dependencies=[Depends(init_file_handler)])
async def read_item(path: str, file_handler: FileHandler = Depends()): # $ Source
return file_handler.get_data(path)
@app.get("/file3/", dependencies=[Depends(init_file_handler)])
async def read_item(path: str): # $ Source
return file_handler.get_data(path)
@app.on_event("startup")
def init_file_handler():
app.state.file_handler1 = FileHandler()
app.state.file_handler2 = FileHandler()
def get_data_source():
return app.state.file_handler1
@app.get("/file4/")
async def read_item(path: str, data_source=Depends(get_data_source)): # $ MISSING: Source
return data_source.get_data(path)
@app.get("/file5/", dependencies=[Depends(init_file_handler)])
async def read_item(path: str): # $ Source
return app.state.file_handler2.get_data(path)

View File

@@ -1,4 +1,4 @@
from flask import Flask, request, send_from_directory
from flask import Flask, request, send_from_directory # $Source
app = Flask(__name__)
@@ -18,4 +18,4 @@ def download_file():
def download_file():
dirname = request.args.get('dirname', '')
filename = request.args.get('filename', '')
return send_from_directory(dirname, filename) # $result=BAD result=OK(filename)
return send_from_directory(dirname, filename) # $ Alert

View File

@@ -1,6 +1,6 @@
import os
from flask import Flask, request
from flask import Flask, request # $ Source
app = Flask(__name__)
@@ -10,7 +10,7 @@ STATIC_DIR = "/server/static/"
@app.route("/path1")
def path_injection():
filename = request.args.get('filename', '')
f = open(os.path.join(STATIC_DIR, filename)) # $result=BAD
f = open(os.path.join(STATIC_DIR, filename)) # $ Alert
@app.route("/path2")
@@ -18,7 +18,7 @@ def path_injection():
# Normalized, but not checked
filename = request.args.get('filename', '')
npath = os.path.normpath(os.path.join(STATIC_DIR, filename))
f = open(npath) # $result=BAD
f = open(npath) # $ Alert
@app.route("/path3")
@@ -28,7 +28,7 @@ def unsafe_path_normpath():
npath = os.path.normpath(os.path.join(STATIC_DIR, filename))
if npath.startswith(STATIC_DIR):
pass
f = open(npath) # $result=BAD
f = open(npath) # $ Alert
@app.route("/path4")
@@ -45,7 +45,7 @@ def unsafe_path_realpath():
# Normalized (by `realpath` that also follows symlinks), but not checked properly
filename = request.args.get('filename', '')
npath = os.path.realpath(os.path.join(STATIC_DIR, filename))
f = open(npath) # $result=BAD
f = open(npath) # $ Alert
@app.route("/path6")
@@ -62,7 +62,7 @@ def unsafe_path_abspath():
# Normalized (by `abspath`), but not checked properly
filename = request.args.get('filename', '')
npath = os.path.abspath(os.path.join(STATIC_DIR, filename))
f = open(npath) # $result=BAD
f = open(npath) # $ Alert
@app.route("/path7")
@@ -84,22 +84,22 @@ def safe_path_abspath_tricky():
filename = request.args.get('filename', '')
possibly_unsafe_path = os.path.join(STATIC_DIR, filename)
if os.path.abspath(possibly_unsafe_path).startswith(STATIC_DIR):
f = open(possibly_unsafe_path) # $SPURIOUS: result=BAD
f = open(possibly_unsafe_path) # $ SPURIOUS: Alert
@app.route("/int-only/<int:foo_id>")
def flask_int_only(foo_id):
def flask_int_only(foo_id): # $ SPURIOUS: Source
# This is OK, since the flask routing ensures that `foo_id` MUST be an integer.
path = os.path.join(STATIC_DIR, foo_id)
f = open(path) # $spurious: result=BAD
f = open(path) # $ SPURIOUS: Alert
@app.route("/not-path/<foo>")
def flask_not_path(foo):
def flask_not_path(foo): # $ Source
# On UNIX systems, this is OK, since without being marked as `<path:foo>`, flask
# routing ensures that `foo` cannot contain forward slashes (not by using %2F either).
path = os.path.join(STATIC_DIR, foo)
f = open(path) # $result=BAD // OK if only running on UNIX systems, NOT OK if could be running on windows
f = open(path) # $ Alert // OK if only running on UNIX systems, NOT OK if could be running on windows
@app.route("/no-dot-dot")
@@ -110,7 +110,7 @@ def no_dot_dot():
# handle if `filename` is an absolute path
if '../' in path:
return "not this time"
f = open(path) # $result=BAD
f = open(path) # $ Alert
@app.route("/no-dot-dot-with-prefix")
@@ -121,7 +121,7 @@ def no_dot_dot_with_prefix():
# Therefore, for UNIX-only programs, the `../` check is enough to stop path injections.
if '../' in path:
return "not this time"
f = open(path) # $result=BAD // OK if only running on UNIX systems, NOT OK if could be running on windows
f = open(path) # $ Alert // OK if only running on UNIX systems, NOT OK if could be running on windows
@app.route("/replace-slash")
@@ -129,7 +129,7 @@ def replace_slash():
filename = request.args.get('filename', '')
path = os.path.join(STATIC_DIR, filename)
sanitized = path.replace("/", "_")
f = open(sanitized) # $result=BAD // OK if only running on UNIX systems, NOT OK if could be running on windows
f = open(sanitized) # $ Alert // OK if only running on UNIX systems, NOT OK if could be running on windows
@app.route("/stackoverflow-solution")
@@ -139,7 +139,7 @@ def stackoverflow_solution():
path = os.path.join(STATIC_DIR, filename)
if os.path.commonprefix((os.path.realpath(path), STATIC_DIR)) != STATIC_DIR:
return "not this time"
f = open(path) # $SPURIOUS: result=BAD
f = open(path) # $ SPURIOUS: Alert
SAFE_FILES = ['foo', 'bar', 'baz']
@@ -149,4 +149,4 @@ def safe_set_of_files():
filename = request.args.get('filename', '')
if filename in SAFE_FILES:
path = os.path.join(STATIC_DIR, filename)
f = open(path) # $SPURIOUS: result=BAD
f = open(path) # $ SPURIOUS: Alert

View File

@@ -1,6 +1,6 @@
import pathlib
from flask import Flask, request
from flask import Flask, request # $ Source
app = Flask(__name__)
@@ -11,7 +11,7 @@ STATIC_DIR = pathlib.Path("/server/static/")
def path_injection():
filename = request.args.get('filename', '')
p = STATIC_DIR / filename
p.open() # $ result=BAD
p.open() # $ Alert
p2 = pathlib.Path(STATIC_DIR, filename)
p2.open() # $ result=BAD
p2.open() # $ Alert

View File

@@ -1,6 +1,6 @@
import os.path
from flask import Flask, request
from flask import Flask, request # $ Source
app = Flask(__name__)
@@ -16,21 +16,21 @@ def normalize(x):
@app.route("/path")
def simple():
x = source()
open(x) # $result=BAD
open(x) # $ Alert
@app.route("/path")
def normalization():
x = source()
y = normalize(x)
open(y) # $result=BAD
open(y) # $ Alert
@app.route("/path")
def check():
x = source()
if x.startswith("subfolder/"):
open(x) # $result=BAD
open(x) # $ Alert
@app.route("/path")
@@ -46,4 +46,4 @@ def check_then_normalize():
x = source()
if x.startswith("subfolder/"):
y = normalize(x)
open(y) # $result=BAD
open(y) # $ Alert