From dcbdd43cf99a611b4ed8ac161e7357571a716aff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 07:54:08 +0000 Subject: [PATCH 1/3] Initial plan From 3a512ae7773b45331fbab2c642e8ab9576ce379e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 08:19:48 +0000 Subject: [PATCH 2/3] Add os.path.basename as a sanitizer for py/path-injection - Add test cases in path_injection.py demonstrating that os.path.basename prevents path traversal attacks (false positive scenarios) - Add OsPathBasenameCall sanitizer class in PathInjectionCustomizations.qll that recognizes calls to os.path.basename (and posixpath/ntpath/genericpath variants) as barriers for the path-injection taint flow os.path.basename strips all directory components from a path, returning only the final filename. This makes it impossible for an attacker to inject path traversal sequences like ../etc/passwd - the basename of such input would just be 'passwd'. Agent-Logs-Url: https://github.com/github/codeql/sessions/6603215b-21cd-4e05-8905-550434c7b9ff Co-authored-by: hvitved <3667920+hvitved@users.noreply.github.com> --- .../dataflow/PathInjectionCustomizations.qll | 22 +++++++++++++++++++ .../CWE-022-PathInjection/path_injection.py | 17 ++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/python/ql/lib/semmle/python/security/dataflow/PathInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/PathInjectionCustomizations.qll index 7121faa19ffb..023e0906002f 100644 --- a/python/ql/lib/semmle/python/security/dataflow/PathInjectionCustomizations.qll +++ b/python/ql/lib/semmle/python/security/dataflow/PathInjectionCustomizations.qll @@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow private import semmle.python.Concepts private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.dataflow.new.BarrierGuards +private import semmle.python.ApiGraphs /** * Provides default sources, and sinks for detecting @@ -105,4 +106,25 @@ module PathInjection { class SanitizerFromModel extends Sanitizer { SanitizerFromModel() { ModelOutput::barrierNode(this, "path-injection") } } + + /** + * A call to `os.path.basename`, considered as a sanitizer for path injection. + * + * `os.path.basename` returns the final component of a path, stripping any + * leading directory components. This prevents path traversal attacks since + * the result cannot contain directory separators or relative path components. + * See https://docs.python.org/3/library/os.path.html#os.path.basename + */ + private class OsPathBasenameCall extends Sanitizer, DataFlow::CallCfgNode { + OsPathBasenameCall() { + exists(API::Node osPathModule | + ( + osPathModule = API::moduleImport("os").getMember("path") + or + osPathModule = API::moduleImport(["posixpath", "ntpath", "genericpath"]) + ) and + this = osPathModule.getMember("basename").getACall() + ) + } + } } diff --git a/python/ql/test/query-tests/Security/CWE-022-PathInjection/path_injection.py b/python/ql/test/query-tests/Security/CWE-022-PathInjection/path_injection.py index bff0d750c9fd..87e40c4d0426 100644 --- a/python/ql/test/query-tests/Security/CWE-022-PathInjection/path_injection.py +++ b/python/ql/test/query-tests/Security/CWE-022-PathInjection/path_injection.py @@ -150,3 +150,20 @@ def safe_set_of_files(): if filename in SAFE_FILES: path = os.path.join(STATIC_DIR, filename) f = open(path) # $ SPURIOUS: Alert + + +@app.route("/basename-sanitizer") +def basename_sanitizer(): + filename = request.args.get('filename', '') + # Secure mitigation pattern: os.path.basename strips all directory components, + # preventing path traversal attacks. + path = os.path.join(STATIC_DIR, os.path.basename(filename)) + f = open(path) # $ result=OK + + +@app.route("/basename-no-join") +def basename_no_join(): + filename = request.args.get('filename', '') + # basename alone also prevents directory traversal + path = os.path.basename(filename) + f = open(path) # $ result=OK From 3229b9052ea12210cdfe0fc9ca3abc08d9f6b9d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:46:07 +0000 Subject: [PATCH 3/3] Apply suggestion: use exists(var | range | formula) idiom in OsPathBasenameCall Agent-Logs-Url: https://github.com/github/codeql/sessions/a319e151-8e8f-4770-b87c-12b5cdb268b8 Co-authored-by: hvitved <3667920+hvitved@users.noreply.github.com> --- .../security/dataflow/PathInjectionCustomizations.qll | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/python/ql/lib/semmle/python/security/dataflow/PathInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/PathInjectionCustomizations.qll index 023e0906002f..c1e77b05e8c5 100644 --- a/python/ql/lib/semmle/python/security/dataflow/PathInjectionCustomizations.qll +++ b/python/ql/lib/semmle/python/security/dataflow/PathInjectionCustomizations.qll @@ -118,11 +118,10 @@ module PathInjection { private class OsPathBasenameCall extends Sanitizer, DataFlow::CallCfgNode { OsPathBasenameCall() { exists(API::Node osPathModule | - ( - osPathModule = API::moduleImport("os").getMember("path") - or - osPathModule = API::moduleImport(["posixpath", "ntpath", "genericpath"]) - ) and + osPathModule = API::moduleImport("os").getMember("path") + or + osPathModule = API::moduleImport(["posixpath", "ntpath", "genericpath"]) + | this = osPathModule.getMember("basename").getACall() ) }