Skip to content

Python: Add jump steps for global variable nested field access #20162

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Python: Add global variable nested field jump steps
  • Loading branch information
Napalys committed Aug 4, 2025
commit 1c4a48e79282181b0aabf3d0e629c79449bfe0af
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,61 @@ 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)
}

/**
* 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`.
*/
predicate globalVariableNestedFieldJumpStep(Node nodeFrom, Node nodeTo) {
exists(GlobalVariable globalVar, AttrWrite write, AttrRead read |
// Match writes and reads on the same global variable attribute path
globalVariableAttrPath(globalVar, write.getObject()) and
globalVariableAttrPath(globalVar, read.getObject()) and
write.getAttributeName() = read.getAttributeName() and
nodeFrom = write.getValue() and
nodeTo = read and
write.getEnclosingCallable() != read.getEnclosingCallable()
)
}

/**
* Maximum depth for global variable nested attribute access.
* Depth 0 = globalVar.foo, depth 1 = globalVar.foo.bar, depth 2 = globalVar.foo.bar.baz, etc.
*/
private int getMaxGlobalVariableDepth() { result = 1 }

/**
* Holds if `node` is an attribute access path starting from global variable `globalVar`.
* Supports configurable nesting depth via getMaxGlobalVariableDepth().
*/
predicate globalVariableAttrPath(GlobalVariable globalVar, Node node) {
globalVariableAttrPathAtDepth(globalVar, node, _)
}

/**
* Holds if `node` is an attribute access path starting from global variable `globalVar` at specific `depth`.
*/
predicate globalVariableAttrPathAtDepth(GlobalVariable globalVar, Node node, int depth) {
// Base case: Direct global variable access (depth 0)
depth = 0 and
exists(NameNode name |
name.getId() = globalVar.getId() and
node.asCfgNode() = name and
name.getNode().(Name).getVariable() instanceof GlobalVariable and
not exists(ClassExpr cls | cls.getName() = globalVar.getId())
)
or
// Recursive case: Nested attribute access (depth > 0)
exists(AttrRead attr, int parentDepth |
globalVariableAttrPathAtDepth(globalVar, attr.getObject(), parentDepth) and
node = attr and
depth = parentDepth + 1 and
depth <= getMaxGlobalVariableDepth()
)
}

//--------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,18 +177,18 @@ def read_global():
def test_global_nested_attributes():
init_global()
result = read_global()
SINK(result) # $ MISSING: flow="SOURCE, l:-8 -> result"
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"
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
| 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 |
Expand Down Expand Up @@ -30,6 +31,8 @@ edges
| 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 | |
Expand Down Expand Up @@ -161,6 +164,8 @@ nodes
| 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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ async def read_item(path: str, data_source=Depends(get_data_source)): # $ MISSIN
return data_source.get_data(path)

@app.get("/file5/", dependencies=[Depends(init_file_handler)])
async def read_item(path: str): # $ MISSING: Source
async def read_item(path: str): # $ Source
return app.state.file_handler2.get_data(path)