From 005db5bdcf9572585a2a902072263136c12de463 Mon Sep 17 00:00:00 2001
From: Kristen Newbury
Date: Fri, 12 Dec 2025 17:41:12 -0500
Subject: [PATCH 01/24] Add first version prompt injection query python openai
agents sdk
---
python/ql/lib/semmle/python/Frameworks.qll | 1 +
.../lib/semmle/python/frameworks/OpenAI.qll | 20 ++++++++++
.../PromptInjectionCustomizations.qll | 37 +++++++++++++++++++
.../dataflow/PromptInjectionQuery.qll | 20 ++++++++++
.../Security/CWE-1427/PromptInjection.qhelp | 25 +++++++++++++
.../src/Security/CWE-1427/PromptInjection.ql | 20 ++++++++++
.../PromptInjection.expected | 18 +++++++++
.../PromptInjection.qlref | 1 +
.../agent_instructions.py | 12 ++++++
9 files changed, 154 insertions(+)
create mode 100644 python/ql/lib/semmle/python/frameworks/OpenAI.qll
create mode 100644 python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
create mode 100644 python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll
create mode 100644 python/ql/src/Security/CWE-1427/PromptInjection.qhelp
create mode 100644 python/ql/src/Security/CWE-1427/PromptInjection.ql
create mode 100644 python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
create mode 100644 python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
create mode 100644 python/ql/test/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
diff --git a/python/ql/lib/semmle/python/Frameworks.qll b/python/ql/lib/semmle/python/Frameworks.qll
index 7694419b41d5..f28686bf2fae 100644
--- a/python/ql/lib/semmle/python/Frameworks.qll
+++ b/python/ql/lib/semmle/python/Frameworks.qll
@@ -54,6 +54,7 @@ private import semmle.python.frameworks.Multidict
private import semmle.python.frameworks.Mysql
private import semmle.python.frameworks.MySQLdb
private import semmle.python.frameworks.Numpy
+private import semmle.python.frameworks.OpenAI
private import semmle.python.frameworks.Opml
private import semmle.python.frameworks.Oracledb
private import semmle.python.frameworks.Pandas
diff --git a/python/ql/lib/semmle/python/frameworks/OpenAI.qll b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
new file mode 100644
index 000000000000..77a4787a5445
--- /dev/null
+++ b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
@@ -0,0 +1,20 @@
+/**
+ * Provides classes modeling security-relevant aspects of the `openAI`Agents SDK package.
+ * See https://github.com/openai/openai-agents-python.
+ */
+
+private import python
+private import semmle.python.ApiGraphs
+
+/**
+ * Provides models for Agent (instances of the `agents.Agent` class).
+ *
+ * See https://github.com/openai/openai-agents-python.
+ */
+module Agent {
+ /** Gets a reference to the `agents.Agent` class. */
+ API::Node classRef() { result = API::moduleImport("agents").getMember("Agent") }
+
+ /** Gets a reference to a potential property of `agents.Agent` called instructions which refers to the system prompt. */
+ API::Node sink() { result = classRef().getACall().getKeywordParameter("instructions") }
+}
diff --git a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
new file mode 100644
index 000000000000..8f1414f5c4a1
--- /dev/null
+++ b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
@@ -0,0 +1,37 @@
+import python
+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.frameworks.OpenAI
+
+/**
+ * Provides default sources, sinks and sanitizers for detecting
+ * "prompt injection"
+ * vulnerabilities, as well as extension points for adding your own.
+ */
+module PromptInjection {
+ /**
+ * A data flow source for "prompt injection" vulnerabilities.
+ */
+ abstract class Source extends DataFlow::Node { }
+
+ /**
+ * A data flow sink for "prompt injection" vulnerabilities.
+ */
+ abstract class Sink extends DataFlow::Node { }
+
+ /**
+ * A sanitizer for "prompt injection" vulnerabilities.
+ */
+ abstract class Sanitizer extends DataFlow::Node { }
+
+ /**
+ * An active threat-model source, considered as a flow source.
+ */
+ private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { }
+
+ class SystemPromptSink extends Sink {
+ SystemPromptSink() { this = Agent::sink().asSink() }
+ }
+}
diff --git a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll
new file mode 100644
index 000000000000..a47d8845c850
--- /dev/null
+++ b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll
@@ -0,0 +1,20 @@
+private import python
+import semmle.python.dataflow.new.DataFlow
+import semmle.python.dataflow.new.TaintTracking
+import PromptInjectionCustomizations::PromptInjection
+
+private module PromptInjectionConfig implements DataFlow::ConfigSig {
+ predicate isSource(DataFlow::Node node) { node instanceof Source }
+
+ predicate isSink(DataFlow::Node node) {
+ node instanceof Sink
+ //any()
+ }
+
+ predicate isBarrierIn(DataFlow::Node node) { node instanceof Sanitizer }
+
+ predicate observeDiffInformedIncrementalMode() { any() }
+}
+
+/** Global taint-tracking for detecting "prompt injection" vulnerabilities. */
+module PromptInjectionFlow = TaintTracking::Global;
diff --git a/python/ql/src/Security/CWE-1427/PromptInjection.qhelp b/python/ql/src/Security/CWE-1427/PromptInjection.qhelp
new file mode 100644
index 000000000000..b07bae4cca67
--- /dev/null
+++ b/python/ql/src/Security/CWE-1427/PromptInjection.qhelp
@@ -0,0 +1,25 @@
+
+
+
+
+Prompts can be constructed to bypass the original purposes of an agent and lead to sensitive data leak or
+operations that were not intended.
+
+
+
+
+Sanitize user input and also avoid using user input in developer or system level prompts.
+
+
+
+In the following examples, the cases marked GOOD show secure prompt construction; whereas in the case marked BAD they may be susceptible to prompt injection.
+
+
+
+
+OWASP: PromptInjection.
+
+
+
diff --git a/python/ql/src/Security/CWE-1427/PromptInjection.ql b/python/ql/src/Security/CWE-1427/PromptInjection.ql
new file mode 100644
index 000000000000..ab51b681a2f8
--- /dev/null
+++ b/python/ql/src/Security/CWE-1427/PromptInjection.ql
@@ -0,0 +1,20 @@
+/**
+ * @name User input used in developer message and or system prompt
+ * @description User input used in developer message and or system prompt can allow for Prompt Injection attacks.
+ * @kind path-problem
+ * @problem.severity error
+ * @security-severity 5.0
+ * @precision high
+ * @id py/prompt-injection
+ * @tags security
+ * external/cwe/cwe-1427
+ */
+
+import python
+import semmle.python.security.dataflow.PromptInjectionQuery
+import PromptInjectionFlow::PathGraph
+
+from PromptInjectionFlow::PathNode source, PromptInjectionFlow::PathNode sink
+where PromptInjectionFlow::flowPath(source, sink)
+select sink.getNode(), source, sink, "This prompt construction depends on a $@.", source.getNode(),
+ "user-provided value"
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
new file mode 100644
index 000000000000..1035cfd357d7
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
@@ -0,0 +1,18 @@
+edges
+| agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | agent_instructions.py:2:26:2:32 | ControlFlowNode for request | provenance | |
+| agent_instructions.py:2:26:2:32 | ControlFlowNode for request | agent_instructions.py:7:13:7:19 | ControlFlowNode for request | provenance | |
+| agent_instructions.py:7:5:7:9 | ControlFlowNode for input | agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | provenance | |
+| agent_instructions.py:7:13:7:19 | ControlFlowNode for request | agent_instructions.py:7:13:7:24 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
+| agent_instructions.py:7:13:7:24 | ControlFlowNode for Attribute | agent_instructions.py:7:13:7:37 | ControlFlowNode for Attribute() | provenance | dict.get |
+| agent_instructions.py:7:13:7:37 | ControlFlowNode for Attribute() | agent_instructions.py:7:5:7:9 | ControlFlowNode for input | provenance | |
+nodes
+| agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
+| agent_instructions.py:2:26:2:32 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| agent_instructions.py:7:5:7:9 | ControlFlowNode for input | semmle.label | ControlFlowNode for input |
+| agent_instructions.py:7:13:7:19 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| agent_instructions.py:7:13:7:24 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| agent_instructions.py:7:13:7:37 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
+| agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
+subpaths
+#select
+| agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
new file mode 100644
index 000000000000..c72fd0eb3b2c
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
@@ -0,0 +1 @@
+query: Security/CWE-1427/PromptInjection.ql
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
new file mode 100644
index 000000000000..c8e9d6dc23c1
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
@@ -0,0 +1,12 @@
+from agents import Agent, Runner
+from flask import Flask, request # $ Source=flask
+app = Flask(__name__)
+
+@app.route("/parameter-route")
+def get_input():
+ input = request.args.get("input")
+
+ agent = Agent(name="Assistant", instructions="This prompt is customized for " + input) # $Alert[py/prompt-injection]
+
+ result = Runner.run_sync(agent, "This is a user message.")
+ print(result.final_output)
\ No newline at end of file
From 7a9e03d1be3171f1ed0789cc4dfd10393e25a3f5 Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Fri, 2 Jan 2026 12:22:16 +0100
Subject: [PATCH 02/24] Add support for `openai.OpenAI` client library
---
python/ql/lib/semmle/python/Frameworks.qll | 1 +
.../lib/semmle/python/frameworks/OpenAI.qll | 59 +++++++++++++
.../PromptInjectionCustomizations.qll | 43 ++++++++++
.../dataflow/PromptInjectionQuery.qll | 20 +++++
.../Security/CWE-1427/PromptInjection.qhelp | 25 ++++++
.../src/Security/CWE-1427/PromptInjection.ql | 20 +++++
.../PromptInjection.expected | 80 ++++++++++++++++++
.../PromptInjection.qlref | 1 +
.../agent_instructions.py | 12 +++
.../CWE-1427-PromptInjection/openai_test.py | 83 +++++++++++++++++++
10 files changed, 344 insertions(+)
create mode 100644 python/ql/lib/semmle/python/frameworks/OpenAI.qll
create mode 100644 python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
create mode 100644 python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll
create mode 100644 python/ql/src/Security/CWE-1427/PromptInjection.qhelp
create mode 100644 python/ql/src/Security/CWE-1427/PromptInjection.ql
create mode 100644 python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
create mode 100644 python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
create mode 100644 python/ql/test/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
create mode 100644 python/ql/test/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
diff --git a/python/ql/lib/semmle/python/Frameworks.qll b/python/ql/lib/semmle/python/Frameworks.qll
index 7694419b41d5..f28686bf2fae 100644
--- a/python/ql/lib/semmle/python/Frameworks.qll
+++ b/python/ql/lib/semmle/python/Frameworks.qll
@@ -54,6 +54,7 @@ private import semmle.python.frameworks.Multidict
private import semmle.python.frameworks.Mysql
private import semmle.python.frameworks.MySQLdb
private import semmle.python.frameworks.Numpy
+private import semmle.python.frameworks.OpenAI
private import semmle.python.frameworks.Opml
private import semmle.python.frameworks.Oracledb
private import semmle.python.frameworks.Pandas
diff --git a/python/ql/lib/semmle/python/frameworks/OpenAI.qll b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
new file mode 100644
index 000000000000..e8abb05ec1ba
--- /dev/null
+++ b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
@@ -0,0 +1,59 @@
+/**
+ * Provides classes modeling security-relevant aspects of the `openAI`Agents SDK package.
+ * See https://github.com/openai/openai-agents-python.
+ */
+
+private import python
+private import semmle.python.ApiGraphs
+
+/**
+ * Provides models for Agent (instances of the `agents.Agent` class).
+ *
+ * See https://github.com/openai/openai-agents-python.
+ */
+module Agent {
+ /** Gets a reference to the `agents.Agent` class. */
+ API::Node classRef() { result = API::moduleImport("agents").getMember("Agent") }
+
+ /** Gets a reference to a potential property of `agents.Agent` called instructions which refers to the system prompt. */
+ API::Node sink() { result = classRef().getACall().getKeywordParameter("instructions") }
+}
+
+/**
+ * Provides models for OpenAI (instances of `openai` classes).
+ *
+ * See https://github.com/openai/openai-python.
+ */
+module OpenAI {
+ /** Gets a reference to `openai.OpenAI`, `openai.AsyncOpenAI` and `openai.AzureOpenAI`classes. */
+ API::Node classRef() {
+ result = API::moduleImport("openai").getMember(["OpenAI", "AsyncOpenAI", "AzureOpenAI"])
+ }
+
+ /** Gets a reference to a potential property of `openai.OpenAI called instructions which refers to the system prompt. */
+ API::Node sink() {
+ result =
+ classRef()
+ .getReturn()
+ .getMember("responses")
+ .getMember("create")
+ .getKeywordParameter(["input", "instructions"]) or
+ result =
+ classRef()
+ .getReturn()
+ .getMember("realtime")
+ .getMember("connect")
+ .getReturn()
+ .getMember("conversation")
+ .getMember("item")
+ .getMember("create")
+ .getKeywordParameter("item") or
+ result =
+ classRef()
+ .getReturn()
+ .getMember("chat")
+ .getMember("completions")
+ .getMember("create")
+ .getKeywordParameter("messages")
+ }
+}
diff --git a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
new file mode 100644
index 000000000000..a00479c8722c
--- /dev/null
+++ b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
@@ -0,0 +1,43 @@
+import python
+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.frameworks.OpenAI
+
+/**
+ * Provides default sources, sinks and sanitizers for detecting
+ * "prompt injection"
+ * vulnerabilities, as well as extension points for adding your own.
+ */
+module PromptInjection {
+ /**
+ * A data flow source for "prompt injection" vulnerabilities.
+ */
+ abstract class Source extends DataFlow::Node { }
+
+ /**
+ * A data flow sink for "prompt injection" vulnerabilities.
+ */
+ abstract class Sink extends DataFlow::Node { }
+
+ /**
+ * A sanitizer for "prompt injection" vulnerabilities.
+ */
+ abstract class Sanitizer extends DataFlow::Node { }
+
+ /**
+ * An active threat-model source, considered as a flow source.
+ */
+ private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { }
+
+ class SystemPromptSink extends Sink {
+ SystemPromptSink() { this = Agent::sink().asSink() or this = OpenAI::sink().asSink() }
+ }
+
+ private import semmle.python.frameworks.data.ModelsAsData
+
+ private class DataAsPromptSink extends Sink {
+ DataAsPromptSink() { this = ModelOutput::getASinkNode("prompt-injection").asSink() }
+ }
+}
diff --git a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll
new file mode 100644
index 000000000000..a47d8845c850
--- /dev/null
+++ b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll
@@ -0,0 +1,20 @@
+private import python
+import semmle.python.dataflow.new.DataFlow
+import semmle.python.dataflow.new.TaintTracking
+import PromptInjectionCustomizations::PromptInjection
+
+private module PromptInjectionConfig implements DataFlow::ConfigSig {
+ predicate isSource(DataFlow::Node node) { node instanceof Source }
+
+ predicate isSink(DataFlow::Node node) {
+ node instanceof Sink
+ //any()
+ }
+
+ predicate isBarrierIn(DataFlow::Node node) { node instanceof Sanitizer }
+
+ predicate observeDiffInformedIncrementalMode() { any() }
+}
+
+/** Global taint-tracking for detecting "prompt injection" vulnerabilities. */
+module PromptInjectionFlow = TaintTracking::Global;
diff --git a/python/ql/src/Security/CWE-1427/PromptInjection.qhelp b/python/ql/src/Security/CWE-1427/PromptInjection.qhelp
new file mode 100644
index 000000000000..b07bae4cca67
--- /dev/null
+++ b/python/ql/src/Security/CWE-1427/PromptInjection.qhelp
@@ -0,0 +1,25 @@
+
+
+
+
+Prompts can be constructed to bypass the original purposes of an agent and lead to sensitive data leak or
+operations that were not intended.
+
+
+
+
+Sanitize user input and also avoid using user input in developer or system level prompts.
+
+
+
+In the following examples, the cases marked GOOD show secure prompt construction; whereas in the case marked BAD they may be susceptible to prompt injection.
+
+
+
+
+OWASP: PromptInjection.
+
+
+
diff --git a/python/ql/src/Security/CWE-1427/PromptInjection.ql b/python/ql/src/Security/CWE-1427/PromptInjection.ql
new file mode 100644
index 000000000000..ab51b681a2f8
--- /dev/null
+++ b/python/ql/src/Security/CWE-1427/PromptInjection.ql
@@ -0,0 +1,20 @@
+/**
+ * @name User input used in developer message and or system prompt
+ * @description User input used in developer message and or system prompt can allow for Prompt Injection attacks.
+ * @kind path-problem
+ * @problem.severity error
+ * @security-severity 5.0
+ * @precision high
+ * @id py/prompt-injection
+ * @tags security
+ * external/cwe/cwe-1427
+ */
+
+import python
+import semmle.python.security.dataflow.PromptInjectionQuery
+import PromptInjectionFlow::PathGraph
+
+from PromptInjectionFlow::PathNode source, PromptInjectionFlow::PathNode sink
+where PromptInjectionFlow::flowPath(source, sink)
+select sink.getNode(), source, sink, "This prompt construction depends on a $@.", source.getNode(),
+ "user-provided value"
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
new file mode 100644
index 000000000000..d163aa2511a0
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
@@ -0,0 +1,80 @@
+edges
+| agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | agent_instructions.py:2:26:2:32 | ControlFlowNode for request | provenance | |
+| agent_instructions.py:2:26:2:32 | ControlFlowNode for request | agent_instructions.py:7:13:7:19 | ControlFlowNode for request | provenance | |
+| agent_instructions.py:7:5:7:9 | ControlFlowNode for input | agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | provenance | |
+| agent_instructions.py:7:13:7:19 | ControlFlowNode for request | agent_instructions.py:7:13:7:24 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
+| agent_instructions.py:7:13:7:24 | ControlFlowNode for Attribute | agent_instructions.py:7:13:7:37 | ControlFlowNode for Attribute() | provenance | dict.get |
+| agent_instructions.py:7:13:7:37 | ControlFlowNode for Attribute() | agent_instructions.py:7:5:7:9 | ControlFlowNode for input | provenance | |
+| openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:2:26:2:32 | ControlFlowNode for request | provenance | |
+| openai_test.py:2:26:2:32 | ControlFlowNode for request | openai_test.py:12:15:12:21 | ControlFlowNode for request | provenance | |
+| openai_test.py:2:26:2:32 | ControlFlowNode for request | openai_test.py:13:13:13:19 | ControlFlowNode for request | provenance | |
+| openai_test.py:2:26:2:32 | ControlFlowNode for request | openai_test.py:14:12:14:18 | ControlFlowNode for request | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:23:15:36:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:40:22:40:46 | ControlFlowNode for BinaryExpr | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:58:18:69:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:73:18:82:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:12:15:12:21 | ControlFlowNode for request | openai_test.py:12:15:12:26 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
+| openai_test.py:12:15:12:21 | ControlFlowNode for request | openai_test.py:13:13:13:24 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
+| openai_test.py:12:15:12:21 | ControlFlowNode for request | openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
+| openai_test.py:12:15:12:26 | ControlFlowNode for Attribute | openai_test.py:12:15:12:41 | ControlFlowNode for Attribute() | provenance | dict.get |
+| openai_test.py:12:15:12:41 | ControlFlowNode for Attribute() | openai_test.py:12:5:12:11 | ControlFlowNode for persona | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:18:15:18:19 | ControlFlowNode for query | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:23:15:36:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:41:15:41:19 | ControlFlowNode for query | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:46:18:54:13 | ControlFlowNode for Dict | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:58:18:69:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:73:18:82:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:13:13:13:19 | ControlFlowNode for request | openai_test.py:13:13:13:24 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
+| openai_test.py:13:13:13:19 | ControlFlowNode for request | openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
+| openai_test.py:13:13:13:24 | ControlFlowNode for Attribute | openai_test.py:13:13:13:37 | ControlFlowNode for Attribute() | provenance | dict.get |
+| openai_test.py:13:13:13:37 | ControlFlowNode for Attribute() | openai_test.py:13:5:13:9 | ControlFlowNode for query | provenance | |
+| openai_test.py:14:5:14:8 | ControlFlowNode for role | openai_test.py:46:18:54:13 | ControlFlowNode for Dict | provenance | |
+| openai_test.py:14:5:14:8 | ControlFlowNode for role | openai_test.py:58:18:69:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:14:12:14:18 | ControlFlowNode for request | openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
+| openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | openai_test.py:14:12:14:35 | ControlFlowNode for Attribute() | provenance | dict.get |
+| openai_test.py:14:12:14:35 | ControlFlowNode for Attribute() | openai_test.py:14:5:14:8 | ControlFlowNode for role | provenance | |
+nodes
+| agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
+| agent_instructions.py:2:26:2:32 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| agent_instructions.py:7:5:7:9 | ControlFlowNode for input | semmle.label | ControlFlowNode for input |
+| agent_instructions.py:7:13:7:19 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| agent_instructions.py:7:13:7:24 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| agent_instructions.py:7:13:7:37 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
+| agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
+| openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
+| openai_test.py:2:26:2:32 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | semmle.label | ControlFlowNode for persona |
+| openai_test.py:12:15:12:21 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| openai_test.py:12:15:12:26 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| openai_test.py:12:15:12:41 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | semmle.label | ControlFlowNode for query |
+| openai_test.py:13:13:13:19 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| openai_test.py:13:13:13:24 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| openai_test.py:13:13:13:37 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
+| openai_test.py:14:5:14:8 | ControlFlowNode for role | semmle.label | ControlFlowNode for role |
+| openai_test.py:14:12:14:18 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| openai_test.py:14:12:14:35 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
+| openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
+| openai_test.py:18:15:18:19 | ControlFlowNode for query | semmle.label | ControlFlowNode for query |
+| openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
+| openai_test.py:23:15:36:9 | ControlFlowNode for List | semmle.label | ControlFlowNode for List |
+| openai_test.py:40:22:40:46 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
+| openai_test.py:41:15:41:19 | ControlFlowNode for query | semmle.label | ControlFlowNode for query |
+| openai_test.py:46:18:54:13 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
+| openai_test.py:58:18:69:9 | ControlFlowNode for List | semmle.label | ControlFlowNode for List |
+| openai_test.py:73:18:82:9 | ControlFlowNode for List | semmle.label | ControlFlowNode for List |
+subpaths
+#select
+| agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:18:15:18:19 | ControlFlowNode for query | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:18:15:18:19 | ControlFlowNode for query | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:23:15:36:9 | ControlFlowNode for List | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:23:15:36:9 | ControlFlowNode for List | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:40:22:40:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:40:22:40:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:41:15:41:19 | ControlFlowNode for query | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:41:15:41:19 | ControlFlowNode for query | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:46:18:54:13 | ControlFlowNode for Dict | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:46:18:54:13 | ControlFlowNode for Dict | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:58:18:69:9 | ControlFlowNode for List | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:58:18:69:9 | ControlFlowNode for List | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:73:18:82:9 | ControlFlowNode for List | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:73:18:82:9 | ControlFlowNode for List | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
new file mode 100644
index 000000000000..c72fd0eb3b2c
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
@@ -0,0 +1 @@
+query: Security/CWE-1427/PromptInjection.ql
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
new file mode 100644
index 000000000000..b986fe25a802
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
@@ -0,0 +1,12 @@
+from agents import Agent, Runner
+from flask import Flask, request # $ Source=flask
+app = Flask(__name__)
+
+@app.route("/parameter-route")
+def get_input():
+ input = request.args.get("input")
+
+ agent = Agent(name="Assistant", instructions="This prompt is customized for " + input) # $Alert[py/prompt-injection]
+
+ result = Runner.run_sync(agent, "This is a user message.")
+ print(result.final_output)
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/openai_test.py b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
new file mode 100644
index 000000000000..c95cdeee8c96
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
@@ -0,0 +1,83 @@
+from openai import OpenAI, AsyncOpenAI, AzureOpenAI
+from flask import Flask, request # $ Source=flask
+app = Flask(__name__)
+
+client = OpenAI()
+async_client = AsyncOpenAI()
+azure_client = AzureOpenAI()
+
+
+@app.route("/openai")
+def get_input_openai():
+ persona = request.args.get("persona")
+ query = request.args.get("query")
+ role = request.args.get("role")
+
+ response1 = client.responses.create(
+ instructions="Talks like a " + persona, # $Alert[py/prompt-injection]
+ input=query, # $Alert[py/prompt-injection]
+ )
+
+ response2 = client.responses.create(
+ instructions="Talks like a " + persona, # $Alert[py/prompt-injection]
+ input=[
+ {
+ "role": "developer",
+ "content": "Talk like a " + persona # $Alert[py/prompt-injection]
+ },
+ {
+ "role": "user",
+ "content": [
+ {"type": "input_text",
+ "text": query # $Alert[py/prompt-injection]
+ }
+ ]
+ }
+ ]
+ )
+
+ response3 = await async_client.responses.create(
+ instructions="Talks like a " + persona, # $Alert[py/prompt-injection]
+ input=query, # $Alert[py/prompt-injection]
+ )
+
+ async with client.realtime.connect(model="gpt-realtime") as connection:
+ await connection.conversation.item.create(
+ item={
+ "type": "message",
+ "role": role, # $Alert[py/prompt-injection]
+ "content": [
+ {"type": "input_text",
+ "text": query # $Alert[py/prompt-injection]
+ }
+ ],
+ }
+ )
+
+ completion1 = client.chat.completions.create(
+ messages=[
+ {"role": "developer",
+ "content": "Talk like a " + persona}, # $Alert[py/prompt-injection]
+ {
+ "role": "user",
+ "content": query, # $Alert[py/prompt-injection]
+ },
+ {
+ "role": role, # $Alert[py/prompt-injection]
+ "content": query, # $Alert[py/prompt-injection]
+ }
+ ]
+ )
+
+ completion2 = azure_client.chat.completions.create(
+ messages=[
+ {
+ "role": "developer",
+ "content": "Talk like a " + persona # $Alert[py/prompt-injection]
+ },
+ {
+ "role": "user",
+ "content": query, # $Alert[py/prompt-injection]
+ }
+ ]
+ )
From 6c5c87e05075ee49977eadd4f71e0630be300bb9 Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Fri, 2 Jan 2026 12:57:03 +0100
Subject: [PATCH 03/24] Fix projcet build errors
---
.../change-notes/2026-01-02-prompt-injection.md | 5 +++++
.../dataflow/PromptInjectionCustomizations.qll | 14 +++++++++-----
.../security/dataflow/PromptInjectionQuery.qll | 8 ++++++++
.../ql/src/Security/CWE-1427/PromptInjection.qhelp | 5 ++---
4 files changed, 24 insertions(+), 8 deletions(-)
create mode 100644 python/ql/lib/change-notes/2026-01-02-prompt-injection.md
diff --git a/python/ql/lib/change-notes/2026-01-02-prompt-injection.md b/python/ql/lib/change-notes/2026-01-02-prompt-injection.md
new file mode 100644
index 000000000000..e2bd4674bd57
--- /dev/null
+++ b/python/ql/lib/change-notes/2026-01-02-prompt-injection.md
@@ -0,0 +1,5 @@
+---
+category: minorAnalysis
+---
+* Added propmpt injection query
+* Added taint flow model and type model for `agents` and `openai` modules.
\ No newline at end of file
diff --git a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
index a00479c8722c..2701ddf6d31c 100644
--- a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
+++ b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
@@ -1,3 +1,9 @@
+/**
+ * Provides default sources, sinks and sanitizers for detecting
+ * "prompt injection"
+ * vulnerabilities, as well as extension points for adding your own.
+ */
+
import python
private import semmle.python.dataflow.new.DataFlow
private import semmle.python.Concepts
@@ -5,11 +11,6 @@ private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.dataflow.new.BarrierGuards
private import semmle.python.frameworks.OpenAI
-/**
- * Provides default sources, sinks and sanitizers for detecting
- * "prompt injection"
- * vulnerabilities, as well as extension points for adding your own.
- */
module PromptInjection {
/**
* A data flow source for "prompt injection" vulnerabilities.
@@ -31,6 +32,9 @@ module PromptInjection {
*/
private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { }
+ /**
+ * Agent prompt sinks, considered as a flow sink.
+ */
class SystemPromptSink extends Sink {
SystemPromptSink() { this = Agent::sink().asSink() or this = OpenAI::sink().asSink() }
}
diff --git a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll
index a47d8845c850..6a154f09aaf9 100644
--- a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll
+++ b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll
@@ -1,3 +1,11 @@
+/**
+ * Provides taint-tracking configurations for detecting "prompt injection" vulnerabilities.
+ *
+ * Note, for performance reasons: only import this file if
+ * `PromptInjection::Configuration` is needed, otherwise
+ * `PromptInjectionCustomizations` should be imported instead.
+ */
+
private import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.TaintTracking
diff --git a/python/ql/src/Security/CWE-1427/PromptInjection.qhelp b/python/ql/src/Security/CWE-1427/PromptInjection.qhelp
index b07bae4cca67..aae2ee4bd3e5 100644
--- a/python/ql/src/Security/CWE-1427/PromptInjection.qhelp
+++ b/python/ql/src/Security/CWE-1427/PromptInjection.qhelp
@@ -5,12 +5,11 @@
Prompts can be constructed to bypass the original purposes of an agent and lead to sensitive data leak or
-operations that were not intended.
-
+operations that were not intended.
-Sanitize user input and also avoid using user input in developer or system level prompts.
+Sanitize user input and also avoid using user input in developer or system level prompts.
From 616698cb4a9c96e53cbc0725693eb37aeac29677 Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Fri, 2 Jan 2026 12:59:49 +0100
Subject: [PATCH 04/24] Fix newline at end of PromptInjection.qlref
---
.../Security/CWE-1427-PromptInjection/PromptInjection.qlref | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
index c72fd0eb3b2c..356982d82fc0 100644
--- a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
+++ b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
@@ -1 +1 @@
-query: Security/CWE-1427/PromptInjection.ql
\ No newline at end of file
+query: Security/CWE-1427/PromptInjection.ql
From 942834d86f4cbe70fa9208fb34288eae9ce53596 Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Fri, 2 Jan 2026 13:05:54 +0100
Subject: [PATCH 05/24] Update
python/ql/lib/semmle/python/frameworks/OpenAI.qll
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
python/ql/lib/semmle/python/frameworks/OpenAI.qll | 6 ------
1 file changed, 6 deletions(-)
diff --git a/python/ql/lib/semmle/python/frameworks/OpenAI.qll b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
index e8abb05ec1ba..eeb97bbad44f 100644
--- a/python/ql/lib/semmle/python/frameworks/OpenAI.qll
+++ b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
@@ -25,12 +25,6 @@ module Agent {
* See https://github.com/openai/openai-python.
*/
module OpenAI {
- /** Gets a reference to `openai.OpenAI`, `openai.AsyncOpenAI` and `openai.AzureOpenAI`classes. */
- API::Node classRef() {
- result = API::moduleImport("openai").getMember(["OpenAI", "AsyncOpenAI", "AzureOpenAI"])
- }
-
- /** Gets a reference to a potential property of `openai.OpenAI called instructions which refers to the system prompt. */
API::Node sink() {
result =
classRef()
From df979da1b6bd4a54443d91fc537aca5c74397fe6 Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Fri, 2 Jan 2026 13:06:05 +0100
Subject: [PATCH 06/24] Update
python/ql/src/Security/CWE-1427/PromptInjection.ql
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
python/ql/src/Security/CWE-1427/PromptInjection.ql | 2 --
1 file changed, 2 deletions(-)
diff --git a/python/ql/src/Security/CWE-1427/PromptInjection.ql b/python/ql/src/Security/CWE-1427/PromptInjection.ql
index ab51b681a2f8..3a87e96249ae 100644
--- a/python/ql/src/Security/CWE-1427/PromptInjection.ql
+++ b/python/ql/src/Security/CWE-1427/PromptInjection.ql
@@ -1,6 +1,4 @@
/**
- * @name User input used in developer message and or system prompt
- * @description User input used in developer message and or system prompt can allow for Prompt Injection attacks.
* @kind path-problem
* @problem.severity error
* @security-severity 5.0
From bacecb7250265341e113378b7306126725faa555 Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Fri, 2 Jan 2026 13:32:33 +0100
Subject: [PATCH 07/24] Add example to qlhelp
---
.../src/Security/CWE-1427/PromptInjection.qhelp | 3 ++-
.../src/Security/CWE-1427/examples/example.py | 17 +++++++++++++++++
2 files changed, 19 insertions(+), 1 deletion(-)
create mode 100644 python/ql/src/Security/CWE-1427/examples/example.py
diff --git a/python/ql/src/Security/CWE-1427/PromptInjection.qhelp b/python/ql/src/Security/CWE-1427/PromptInjection.qhelp
index aae2ee4bd3e5..ec12b58443f1 100644
--- a/python/ql/src/Security/CWE-1427/PromptInjection.qhelp
+++ b/python/ql/src/Security/CWE-1427/PromptInjection.qhelp
@@ -14,11 +14,12 @@ operations that were not intended.
In the following examples, the cases marked GOOD show secure prompt construction; whereas in the case marked BAD they may be susceptible to prompt injection.
-
+
OWASP: PromptInjection.
+OpenAI: Guardrails.
diff --git a/python/ql/src/Security/CWE-1427/examples/example.py b/python/ql/src/Security/CWE-1427/examples/example.py
new file mode 100644
index 000000000000..e27da5ad19be
--- /dev/null
+++ b/python/ql/src/Security/CWE-1427/examples/example.py
@@ -0,0 +1,17 @@
+from flask import Flask, request
+from agents import Agent, Runner
+from guardrails import GuardrailAgent
+
+@app.route("/parameter-route")
+def get_input():
+ input = request.args.get("input")
+
+ goodAgent = GuardrailAgent( # GOOD: AGent created with guardrails automatically configured.
+ config=Path("guardrails_config.json"),
+ name="Assistant",
+ instructions="This prompt is customized for " + input)
+
+ badAgent = Agent(
+ name="Assistant",
+ instructions="This prompt is customized for " + input # BAD: user input in agent instruction.
+ )
From a9d0a1639a3cca767650e21d1d016ed2e3ffa48e Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Fri, 2 Jan 2026 13:59:34 +0100
Subject: [PATCH 08/24] Fix missing predicate
---
python/ql/lib/semmle/python/frameworks/OpenAI.qll | 6 ++++++
.../dataflow/PromptInjectionCustomizations.qll | 12 ++++++++----
python/ql/src/Security/CWE-1427/PromptInjection.ql | 1 +
3 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/python/ql/lib/semmle/python/frameworks/OpenAI.qll b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
index eeb97bbad44f..e8abb05ec1ba 100644
--- a/python/ql/lib/semmle/python/frameworks/OpenAI.qll
+++ b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
@@ -25,6 +25,12 @@ module Agent {
* See https://github.com/openai/openai-python.
*/
module OpenAI {
+ /** Gets a reference to `openai.OpenAI`, `openai.AsyncOpenAI` and `openai.AzureOpenAI`classes. */
+ API::Node classRef() {
+ result = API::moduleImport("openai").getMember(["OpenAI", "AsyncOpenAI", "AzureOpenAI"])
+ }
+
+ /** Gets a reference to a potential property of `openai.OpenAI called instructions which refers to the system prompt. */
API::Node sink() {
result =
classRef()
diff --git a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
index 2701ddf6d31c..a3efa9884c52 100644
--- a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
+++ b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
@@ -10,7 +10,13 @@ private import semmle.python.Concepts
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.dataflow.new.BarrierGuards
private import semmle.python.frameworks.OpenAI
+private import semmle.python.frameworks.data.ModelsAsData
+/**
+ * Provides default sources, sinks and sanitizers for detecting
+ * "prompt injection"
+ * vulnerabilities, as well as extension points for adding your own.
+ */
module PromptInjection {
/**
* A data flow source for "prompt injection" vulnerabilities.
@@ -39,9 +45,7 @@ module PromptInjection {
SystemPromptSink() { this = Agent::sink().asSink() or this = OpenAI::sink().asSink() }
}
- private import semmle.python.frameworks.data.ModelsAsData
-
- private class DataAsPromptSink extends Sink {
- DataAsPromptSink() { this = ModelOutput::getASinkNode("prompt-injection").asSink() }
+ private class SinkFromModel extends Sink {
+ SinkFromModel() { this = ModelOutput::getASinkNode("prompt-injection").asSink() }
}
}
diff --git a/python/ql/src/Security/CWE-1427/PromptInjection.ql b/python/ql/src/Security/CWE-1427/PromptInjection.ql
index 3a87e96249ae..323034b0331a 100644
--- a/python/ql/src/Security/CWE-1427/PromptInjection.ql
+++ b/python/ql/src/Security/CWE-1427/PromptInjection.ql
@@ -1,4 +1,5 @@
/**
+ * @name Prompt injection
* @kind path-problem
* @problem.severity error
* @security-severity 5.0
From 04193f4bb585eeef90da5c9df35fedd77e3d6558 Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Mon, 5 Jan 2026 20:46:42 +0100
Subject: [PATCH 09/24] Une inline expectations
---
.../PromptInjectionCustomizations.qll | 7 ++-
.../dataflow/PromptInjectionQuery.qll | 9 +--
.../PromptInjection.expected | 56 +++++++++----------
.../PromptInjection.qlref | 1 +
.../agent_instructions.py | 2 +-
.../CWE-1427-PromptInjection/openai_test.py | 45 +++++++--------
6 files changed, 62 insertions(+), 58 deletions(-)
diff --git a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
index a3efa9884c52..d22422d53c9c 100644
--- a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
+++ b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
@@ -42,10 +42,15 @@ module PromptInjection {
* Agent prompt sinks, considered as a flow sink.
*/
class SystemPromptSink extends Sink {
- SystemPromptSink() { this = Agent::sink().asSink() or this = OpenAI::sink().asSink() }
+ SystemPromptSink() { this = [Agent::sink(), OpenAI::sink()].asSink() }
}
private class SinkFromModel extends Sink {
SinkFromModel() { this = ModelOutput::getASinkNode("prompt-injection").asSink() }
}
+
+ /**
+ * A comparison with a constant, considered as a sanitizer-guard.
+ */
+ class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
}
diff --git a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll
index 6a154f09aaf9..5c0413726e62 100644
--- a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll
+++ b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionQuery.qll
@@ -1,5 +1,5 @@
/**
- * Provides taint-tracking configurations for detecting "prompt injection" vulnerabilities.
+ * Provides a taint-tracking configuration for detecting "prompt injection" vulnerabilities.
*
* Note, for performance reasons: only import this file if
* `PromptInjection::Configuration` is needed, otherwise
@@ -14,12 +14,9 @@ import PromptInjectionCustomizations::PromptInjection
private module PromptInjectionConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node node) { node instanceof Source }
- predicate isSink(DataFlow::Node node) {
- node instanceof Sink
- //any()
- }
+ predicate isSink(DataFlow::Node node) { node instanceof Sink }
- predicate isBarrierIn(DataFlow::Node node) { node instanceof Sanitizer }
+ predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }
predicate observeDiffInformedIncrementalMode() { any() }
}
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
index d163aa2511a0..c1b25bb7ca7e 100644
--- a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
+++ b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
@@ -1,3 +1,14 @@
+#select
+| agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:18:15:18:19 | ControlFlowNode for query | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:18:15:18:19 | ControlFlowNode for query | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:23:15:37:9 | ControlFlowNode for List | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:23:15:37:9 | ControlFlowNode for List | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:41:22:41:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:41:22:41:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:42:15:42:19 | ControlFlowNode for query | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:42:15:42:19 | ControlFlowNode for query | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:47:18:55:13 | ControlFlowNode for Dict | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:47:18:55:13 | ControlFlowNode for Dict | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:59:18:70:9 | ControlFlowNode for List | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:59:18:70:9 | ControlFlowNode for List | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:74:18:83:9 | ControlFlowNode for List | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:74:18:83:9 | ControlFlowNode for List | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
edges
| agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | agent_instructions.py:2:26:2:32 | ControlFlowNode for request | provenance | |
| agent_instructions.py:2:26:2:32 | ControlFlowNode for request | agent_instructions.py:7:13:7:19 | ControlFlowNode for request | provenance | |
@@ -11,27 +22,27 @@ edges
| openai_test.py:2:26:2:32 | ControlFlowNode for request | openai_test.py:14:12:14:18 | ControlFlowNode for request | provenance | |
| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | provenance | |
| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | provenance | |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:23:15:36:9 | ControlFlowNode for List | provenance | |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:40:22:40:46 | ControlFlowNode for BinaryExpr | provenance | |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:58:18:69:9 | ControlFlowNode for List | provenance | |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:73:18:82:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:23:15:37:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:41:22:41:46 | ControlFlowNode for BinaryExpr | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:59:18:70:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:74:18:83:9 | ControlFlowNode for List | provenance | |
| openai_test.py:12:15:12:21 | ControlFlowNode for request | openai_test.py:12:15:12:26 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:12:15:12:21 | ControlFlowNode for request | openai_test.py:13:13:13:24 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:12:15:12:21 | ControlFlowNode for request | openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:12:15:12:26 | ControlFlowNode for Attribute | openai_test.py:12:15:12:41 | ControlFlowNode for Attribute() | provenance | dict.get |
| openai_test.py:12:15:12:41 | ControlFlowNode for Attribute() | openai_test.py:12:5:12:11 | ControlFlowNode for persona | provenance | |
| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:18:15:18:19 | ControlFlowNode for query | provenance | |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:23:15:36:9 | ControlFlowNode for List | provenance | |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:41:15:41:19 | ControlFlowNode for query | provenance | |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:46:18:54:13 | ControlFlowNode for Dict | provenance | |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:58:18:69:9 | ControlFlowNode for List | provenance | |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:73:18:82:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:23:15:37:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:42:15:42:19 | ControlFlowNode for query | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:47:18:55:13 | ControlFlowNode for Dict | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:59:18:70:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:74:18:83:9 | ControlFlowNode for List | provenance | |
| openai_test.py:13:13:13:19 | ControlFlowNode for request | openai_test.py:13:13:13:24 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:13:13:13:19 | ControlFlowNode for request | openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:13:13:13:24 | ControlFlowNode for Attribute | openai_test.py:13:13:13:37 | ControlFlowNode for Attribute() | provenance | dict.get |
| openai_test.py:13:13:13:37 | ControlFlowNode for Attribute() | openai_test.py:13:5:13:9 | ControlFlowNode for query | provenance | |
-| openai_test.py:14:5:14:8 | ControlFlowNode for role | openai_test.py:46:18:54:13 | ControlFlowNode for Dict | provenance | |
-| openai_test.py:14:5:14:8 | ControlFlowNode for role | openai_test.py:58:18:69:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:14:5:14:8 | ControlFlowNode for role | openai_test.py:47:18:55:13 | ControlFlowNode for Dict | provenance | |
+| openai_test.py:14:5:14:8 | ControlFlowNode for role | openai_test.py:59:18:70:9 | ControlFlowNode for List | provenance | |
| openai_test.py:14:12:14:18 | ControlFlowNode for request | openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | openai_test.py:14:12:14:35 | ControlFlowNode for Attribute() | provenance | dict.get |
| openai_test.py:14:12:14:35 | ControlFlowNode for Attribute() | openai_test.py:14:5:14:8 | ControlFlowNode for role | provenance | |
@@ -60,21 +71,10 @@ nodes
| openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| openai_test.py:18:15:18:19 | ControlFlowNode for query | semmle.label | ControlFlowNode for query |
| openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
-| openai_test.py:23:15:36:9 | ControlFlowNode for List | semmle.label | ControlFlowNode for List |
-| openai_test.py:40:22:40:46 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
-| openai_test.py:41:15:41:19 | ControlFlowNode for query | semmle.label | ControlFlowNode for query |
-| openai_test.py:46:18:54:13 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
-| openai_test.py:58:18:69:9 | ControlFlowNode for List | semmle.label | ControlFlowNode for List |
-| openai_test.py:73:18:82:9 | ControlFlowNode for List | semmle.label | ControlFlowNode for List |
+| openai_test.py:23:15:37:9 | ControlFlowNode for List | semmle.label | ControlFlowNode for List |
+| openai_test.py:41:22:41:46 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
+| openai_test.py:42:15:42:19 | ControlFlowNode for query | semmle.label | ControlFlowNode for query |
+| openai_test.py:47:18:55:13 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
+| openai_test.py:59:18:70:9 | ControlFlowNode for List | semmle.label | ControlFlowNode for List |
+| openai_test.py:74:18:83:9 | ControlFlowNode for List | semmle.label | ControlFlowNode for List |
subpaths
-#select
-| agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
-| openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
-| openai_test.py:18:15:18:19 | ControlFlowNode for query | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:18:15:18:19 | ControlFlowNode for query | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
-| openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
-| openai_test.py:23:15:36:9 | ControlFlowNode for List | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:23:15:36:9 | ControlFlowNode for List | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
-| openai_test.py:40:22:40:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:40:22:40:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
-| openai_test.py:41:15:41:19 | ControlFlowNode for query | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:41:15:41:19 | ControlFlowNode for query | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
-| openai_test.py:46:18:54:13 | ControlFlowNode for Dict | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:46:18:54:13 | ControlFlowNode for Dict | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
-| openai_test.py:58:18:69:9 | ControlFlowNode for List | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:58:18:69:9 | ControlFlowNode for List | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
-| openai_test.py:73:18:82:9 | ControlFlowNode for List | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:73:18:82:9 | ControlFlowNode for List | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
index 356982d82fc0..04d00b1b4b8c 100644
--- a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
+++ b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
@@ -1 +1,2 @@
query: Security/CWE-1427/PromptInjection.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
index b986fe25a802..ebe26942ce43 100644
--- a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
+++ b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
@@ -1,5 +1,5 @@
from agents import Agent, Runner
-from flask import Flask, request # $ Source=flask
+from flask import Flask, request # $ Source
app = Flask(__name__)
@app.route("/parameter-route")
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/openai_test.py b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
index c95cdeee8c96..f5b29a25dd56 100644
--- a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
+++ b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
@@ -1,5 +1,5 @@
from openai import OpenAI, AsyncOpenAI, AzureOpenAI
-from flask import Flask, request # $ Source=flask
+from flask import Flask, request # $ Source
app = Flask(__name__)
client = OpenAI()
@@ -14,70 +14,71 @@ def get_input_openai():
role = request.args.get("role")
response1 = client.responses.create(
- instructions="Talks like a " + persona, # $Alert[py/prompt-injection]
- input=query, # $Alert[py/prompt-injection]
+ instructions="Talks like a " + persona, # $ Alert[py/prompt-injection]
+ input=query, # $ Alert[py/prompt-injection]
)
response2 = client.responses.create(
- instructions="Talks like a " + persona, # $Alert[py/prompt-injection]
+ instructions="Talks like a " + persona, # $ Alert[py/prompt-injection]
input=[
{
"role": "developer",
- "content": "Talk like a " + persona # $Alert[py/prompt-injection]
+ "content": "Talk like a " + persona
},
{
"role": "user",
"content": [
- {"type": "input_text",
- "text": query # $Alert[py/prompt-injection]
- }
+ {
+ "type": "input_text",
+ "text": query
+ }
]
}
- ]
+ ] # $ Alert[py/prompt-injection]
)
response3 = await async_client.responses.create(
- instructions="Talks like a " + persona, # $Alert[py/prompt-injection]
- input=query, # $Alert[py/prompt-injection]
+ instructions="Talks like a " + persona, # $ Alert[py/prompt-injection]
+ input=query, # $ Alert[py/prompt-injection]
)
async with client.realtime.connect(model="gpt-realtime") as connection:
await connection.conversation.item.create(
item={
"type": "message",
- "role": role, # $Alert[py/prompt-injection]
+ "role": role,
"content": [
{"type": "input_text",
- "text": query # $Alert[py/prompt-injection]
+ "text": query
}
],
- }
+ } # $ Alert[py/prompt-injection]
)
completion1 = client.chat.completions.create(
messages=[
{"role": "developer",
- "content": "Talk like a " + persona}, # $Alert[py/prompt-injection]
+ "content": "Talk like a " + persona},
{
"role": "user",
- "content": query, # $Alert[py/prompt-injection]
+ "content": query,
},
{
- "role": role, # $Alert[py/prompt-injection]
- "content": query, # $Alert[py/prompt-injection]
+ "role": role,
+ "content": query,
}
- ]
+ ] # $ Alert[py/prompt-injection]
)
completion2 = azure_client.chat.completions.create(
messages=[
{
"role": "developer",
- "content": "Talk like a " + persona # $Alert[py/prompt-injection]
+ "content": "Talk like a " + persona
},
{
"role": "user",
- "content": query, # $Alert[py/prompt-injection]
+ "content": query,
}
- ]
+ ] # $ Alert[py/prompt-injection]
)
From 2c83dc3689ed94c8450d74db50b2b278296e12da Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Mon, 5 Jan 2026 22:34:26 +0100
Subject: [PATCH 10/24] Use models as data
---
python/ql/lib/semmle/python/Concepts.qll | 25 ++++++++
python/ql/lib/semmle/python/Frameworks.qll | 1 -
.../lib/semmle/python/frameworks/OpenAI.qll | 59 -------------------
.../semmle/python/frameworks/agent.model.yml | 6 ++
.../semmle/python/frameworks/openai.model.yml | 14 +++++
.../PromptInjectionCustomizations.qll | 7 +--
.../PromptInjection.expected | 30 +++++-----
7 files changed, 63 insertions(+), 79 deletions(-)
delete mode 100644 python/ql/lib/semmle/python/frameworks/OpenAI.qll
create mode 100644 python/ql/lib/semmle/python/frameworks/agent.model.yml
create mode 100644 python/ql/lib/semmle/python/frameworks/openai.model.yml
diff --git a/python/ql/lib/semmle/python/Concepts.qll b/python/ql/lib/semmle/python/Concepts.qll
index 0ca8a4dbef01..f7186bc0175b 100644
--- a/python/ql/lib/semmle/python/Concepts.qll
+++ b/python/ql/lib/semmle/python/Concepts.qll
@@ -325,6 +325,31 @@ private class EncodingAdditionalTaintStep extends TaintTracking::AdditionalTaint
}
}
+/**
+ * A data-flow node that prompts an AI model.
+ *
+ * Extend this class to refine existing API models. If you want to model new APIs,
+ * extend `AIPrompt::Range` instead.
+ */
+class AIPrompt extends DataFlow::Node instanceof AIPrompt::Range {
+ /** Gets an input that is used as AI prompt. */
+ DataFlow::Node getAPrompt() { result = super.getAPrompt() }
+}
+
+/** Provides a class for modeling new AI prompting mechanisms. */
+module AIPrompt {
+ /**
+ * A data-flow node that prompts an AI model.
+ *
+ * Extend this class to model new APIs. If you want to refine existing API models,
+ * extend `AIPrompt` instead.
+ */
+ abstract class Range extends DataFlow::Node {
+ /** Gets an input that is logged. */
+ abstract DataFlow::Node getAPrompt();
+ }
+}
+
/**
* A data-flow node that logs data.
*
diff --git a/python/ql/lib/semmle/python/Frameworks.qll b/python/ql/lib/semmle/python/Frameworks.qll
index f28686bf2fae..7694419b41d5 100644
--- a/python/ql/lib/semmle/python/Frameworks.qll
+++ b/python/ql/lib/semmle/python/Frameworks.qll
@@ -54,7 +54,6 @@ private import semmle.python.frameworks.Multidict
private import semmle.python.frameworks.Mysql
private import semmle.python.frameworks.MySQLdb
private import semmle.python.frameworks.Numpy
-private import semmle.python.frameworks.OpenAI
private import semmle.python.frameworks.Opml
private import semmle.python.frameworks.Oracledb
private import semmle.python.frameworks.Pandas
diff --git a/python/ql/lib/semmle/python/frameworks/OpenAI.qll b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
deleted file mode 100644
index e8abb05ec1ba..000000000000
--- a/python/ql/lib/semmle/python/frameworks/OpenAI.qll
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * Provides classes modeling security-relevant aspects of the `openAI`Agents SDK package.
- * See https://github.com/openai/openai-agents-python.
- */
-
-private import python
-private import semmle.python.ApiGraphs
-
-/**
- * Provides models for Agent (instances of the `agents.Agent` class).
- *
- * See https://github.com/openai/openai-agents-python.
- */
-module Agent {
- /** Gets a reference to the `agents.Agent` class. */
- API::Node classRef() { result = API::moduleImport("agents").getMember("Agent") }
-
- /** Gets a reference to a potential property of `agents.Agent` called instructions which refers to the system prompt. */
- API::Node sink() { result = classRef().getACall().getKeywordParameter("instructions") }
-}
-
-/**
- * Provides models for OpenAI (instances of `openai` classes).
- *
- * See https://github.com/openai/openai-python.
- */
-module OpenAI {
- /** Gets a reference to `openai.OpenAI`, `openai.AsyncOpenAI` and `openai.AzureOpenAI`classes. */
- API::Node classRef() {
- result = API::moduleImport("openai").getMember(["OpenAI", "AsyncOpenAI", "AzureOpenAI"])
- }
-
- /** Gets a reference to a potential property of `openai.OpenAI called instructions which refers to the system prompt. */
- API::Node sink() {
- result =
- classRef()
- .getReturn()
- .getMember("responses")
- .getMember("create")
- .getKeywordParameter(["input", "instructions"]) or
- result =
- classRef()
- .getReturn()
- .getMember("realtime")
- .getMember("connect")
- .getReturn()
- .getMember("conversation")
- .getMember("item")
- .getMember("create")
- .getKeywordParameter("item") or
- result =
- classRef()
- .getReturn()
- .getMember("chat")
- .getMember("completions")
- .getMember("create")
- .getKeywordParameter("messages")
- }
-}
diff --git a/python/ql/lib/semmle/python/frameworks/agent.model.yml b/python/ql/lib/semmle/python/frameworks/agent.model.yml
new file mode 100644
index 000000000000..5a923a335197
--- /dev/null
+++ b/python/ql/lib/semmle/python/frameworks/agent.model.yml
@@ -0,0 +1,6 @@
+extensions:
+ - addsTo:
+ pack: codeql/python-all
+ extensible: sinkModel
+ data:
+ - ['agents', 'Member[Agent].Argument[instructions:]', 'prompt-injection']
diff --git a/python/ql/lib/semmle/python/frameworks/openai.model.yml b/python/ql/lib/semmle/python/frameworks/openai.model.yml
new file mode 100644
index 000000000000..0f4f15677673
--- /dev/null
+++ b/python/ql/lib/semmle/python/frameworks/openai.model.yml
@@ -0,0 +1,14 @@
+extensions:
+ - addsTo:
+ pack: codeql/python-all
+ extensible: sinkModel
+ data:
+ - ['OpenAI', 'Member[responses].Member[create].Argument[input:,instructions:]', 'prompt-injection']
+ - ['OpenAI', 'Member[realtime].Member[connect].ReturnValue.Member[conversation].Member[item].Member[create].Argument[item:]', 'prompt-injection']
+ - ['OpenAI', 'Member[chat].Member[completions].Member[create].Argument[messages:]', 'prompt-injection']
+
+ - addsTo:
+ pack: codeql/python-all
+ extensible: typeModel
+ data:
+ - ['OpenAI', 'openai', 'Member[OpenAI,AsyncOpenAI,AzureOpenAI].ReturnValue']
diff --git a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
index d22422d53c9c..5ea14283026b 100644
--- a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
+++ b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
@@ -9,7 +9,6 @@ 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.frameworks.OpenAI
private import semmle.python.frameworks.data.ModelsAsData
/**
@@ -39,10 +38,10 @@ module PromptInjection {
private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { }
/**
- * Agent prompt sinks, considered as a flow sink.
+ * A prompt to an AI model, considered as a flow sink.
*/
- class SystemPromptSink extends Sink {
- SystemPromptSink() { this = [Agent::sink(), OpenAI::sink()].asSink() }
+ class AIPromptAsSink extends Sink {
+ AIPromptAsSink() { this = any(AIPrompt p).getAPrompt() }
}
private class SinkFromModel extends Sink {
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
index c1b25bb7ca7e..6742141dfb0f 100644
--- a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
+++ b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
@@ -12,7 +12,7 @@
edges
| agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | agent_instructions.py:2:26:2:32 | ControlFlowNode for request | provenance | |
| agent_instructions.py:2:26:2:32 | ControlFlowNode for request | agent_instructions.py:7:13:7:19 | ControlFlowNode for request | provenance | |
-| agent_instructions.py:7:5:7:9 | ControlFlowNode for input | agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | provenance | |
+| agent_instructions.py:7:5:7:9 | ControlFlowNode for input | agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | provenance | Sink:MaD:93 |
| agent_instructions.py:7:13:7:19 | ControlFlowNode for request | agent_instructions.py:7:13:7:24 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| agent_instructions.py:7:13:7:24 | ControlFlowNode for Attribute | agent_instructions.py:7:13:7:37 | ControlFlowNode for Attribute() | provenance | dict.get |
| agent_instructions.py:7:13:7:37 | ControlFlowNode for Attribute() | agent_instructions.py:7:5:7:9 | ControlFlowNode for input | provenance | |
@@ -20,29 +20,29 @@ edges
| openai_test.py:2:26:2:32 | ControlFlowNode for request | openai_test.py:12:15:12:21 | ControlFlowNode for request | provenance | |
| openai_test.py:2:26:2:32 | ControlFlowNode for request | openai_test.py:13:13:13:19 | ControlFlowNode for request | provenance | |
| openai_test.py:2:26:2:32 | ControlFlowNode for request | openai_test.py:14:12:14:18 | ControlFlowNode for request | provenance | |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | provenance | |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | provenance | |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:23:15:37:9 | ControlFlowNode for List | provenance | |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:41:22:41:46 | ControlFlowNode for BinaryExpr | provenance | |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:59:18:70:9 | ControlFlowNode for List | provenance | |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:74:18:83:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | provenance | Sink:MaD:58613 |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | provenance | Sink:MaD:58613 |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:23:15:37:9 | ControlFlowNode for List | provenance | Sink:MaD:58613 |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:41:22:41:46 | ControlFlowNode for BinaryExpr | provenance | Sink:MaD:58613 |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:59:18:70:9 | ControlFlowNode for List | provenance | Sink:MaD:58615 |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:74:18:83:9 | ControlFlowNode for List | provenance | Sink:MaD:58615 |
| openai_test.py:12:15:12:21 | ControlFlowNode for request | openai_test.py:12:15:12:26 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:12:15:12:21 | ControlFlowNode for request | openai_test.py:13:13:13:24 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:12:15:12:21 | ControlFlowNode for request | openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:12:15:12:26 | ControlFlowNode for Attribute | openai_test.py:12:15:12:41 | ControlFlowNode for Attribute() | provenance | dict.get |
| openai_test.py:12:15:12:41 | ControlFlowNode for Attribute() | openai_test.py:12:5:12:11 | ControlFlowNode for persona | provenance | |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:18:15:18:19 | ControlFlowNode for query | provenance | |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:23:15:37:9 | ControlFlowNode for List | provenance | |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:42:15:42:19 | ControlFlowNode for query | provenance | |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:47:18:55:13 | ControlFlowNode for Dict | provenance | |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:59:18:70:9 | ControlFlowNode for List | provenance | |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:74:18:83:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:18:15:18:19 | ControlFlowNode for query | provenance | Sink:MaD:58613 |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:23:15:37:9 | ControlFlowNode for List | provenance | Sink:MaD:58613 |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:42:15:42:19 | ControlFlowNode for query | provenance | Sink:MaD:58613 |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:47:18:55:13 | ControlFlowNode for Dict | provenance | Sink:MaD:58614 |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:59:18:70:9 | ControlFlowNode for List | provenance | Sink:MaD:58615 |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:74:18:83:9 | ControlFlowNode for List | provenance | Sink:MaD:58615 |
| openai_test.py:13:13:13:19 | ControlFlowNode for request | openai_test.py:13:13:13:24 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:13:13:13:19 | ControlFlowNode for request | openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:13:13:13:24 | ControlFlowNode for Attribute | openai_test.py:13:13:13:37 | ControlFlowNode for Attribute() | provenance | dict.get |
| openai_test.py:13:13:13:37 | ControlFlowNode for Attribute() | openai_test.py:13:5:13:9 | ControlFlowNode for query | provenance | |
-| openai_test.py:14:5:14:8 | ControlFlowNode for role | openai_test.py:47:18:55:13 | ControlFlowNode for Dict | provenance | |
-| openai_test.py:14:5:14:8 | ControlFlowNode for role | openai_test.py:59:18:70:9 | ControlFlowNode for List | provenance | |
+| openai_test.py:14:5:14:8 | ControlFlowNode for role | openai_test.py:47:18:55:13 | ControlFlowNode for Dict | provenance | Sink:MaD:58614 |
+| openai_test.py:14:5:14:8 | ControlFlowNode for role | openai_test.py:59:18:70:9 | ControlFlowNode for List | provenance | Sink:MaD:58615 |
| openai_test.py:14:12:14:18 | ControlFlowNode for request | openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | openai_test.py:14:12:14:35 | ControlFlowNode for Attribute() | provenance | dict.get |
| openai_test.py:14:12:14:35 | ControlFlowNode for Attribute() | openai_test.py:14:5:14:8 | ControlFlowNode for role | provenance | |
From 0c7996eb7efe549d0745fc6e50fcf22f1c303b1a Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Tue, 6 Jan 2026 11:39:46 +0100
Subject: [PATCH 11/24] Update
python/ql/src/Security/CWE-1427/examples/example.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
python/ql/src/Security/CWE-1427/examples/example.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/python/ql/src/Security/CWE-1427/examples/example.py b/python/ql/src/Security/CWE-1427/examples/example.py
index e27da5ad19be..7319513b53fd 100644
--- a/python/ql/src/Security/CWE-1427/examples/example.py
+++ b/python/ql/src/Security/CWE-1427/examples/example.py
@@ -6,7 +6,6 @@
def get_input():
input = request.args.get("input")
- goodAgent = GuardrailAgent( # GOOD: AGent created with guardrails automatically configured.
config=Path("guardrails_config.json"),
name="Assistant",
instructions="This prompt is customized for " + input)
From 21a21469f1afd7decd76e3efee785621223e8530 Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Tue, 6 Jan 2026 11:40:51 +0100
Subject: [PATCH 12/24] Update python/ql/lib/semmle/python/Concepts.qll
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
python/ql/lib/semmle/python/Concepts.qll | 1 -
1 file changed, 1 deletion(-)
diff --git a/python/ql/lib/semmle/python/Concepts.qll b/python/ql/lib/semmle/python/Concepts.qll
index f7186bc0175b..1fc51a06d59b 100644
--- a/python/ql/lib/semmle/python/Concepts.qll
+++ b/python/ql/lib/semmle/python/Concepts.qll
@@ -345,7 +345,6 @@ module AIPrompt {
* extend `AIPrompt` instead.
*/
abstract class Range extends DataFlow::Node {
- /** Gets an input that is logged. */
abstract DataFlow::Node getAPrompt();
}
}
From 7d450c580b66c3bbd0785549d61dbe3f57985adc Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Tue, 6 Jan 2026 11:41:12 +0100
Subject: [PATCH 13/24] Update
python/ql/src/Security/CWE-1427/PromptInjection.qhelp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
python/ql/src/Security/CWE-1427/PromptInjection.qhelp | 1 -
1 file changed, 1 deletion(-)
diff --git a/python/ql/src/Security/CWE-1427/PromptInjection.qhelp b/python/ql/src/Security/CWE-1427/PromptInjection.qhelp
index ec12b58443f1..ef6b9c83ac26 100644
--- a/python/ql/src/Security/CWE-1427/PromptInjection.qhelp
+++ b/python/ql/src/Security/CWE-1427/PromptInjection.qhelp
@@ -18,7 +18,6 @@ operations that were not intended.
-OWASP: PromptInjection.
OpenAI: Guardrails.
From c352ffd28c043484170e146eb4f00f8af0482680 Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Tue, 6 Jan 2026 11:41:25 +0100
Subject: [PATCH 14/24] Update
python/ql/lib/change-notes/2026-01-02-prompt-injection.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
python/ql/lib/change-notes/2026-01-02-prompt-injection.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/python/ql/lib/change-notes/2026-01-02-prompt-injection.md b/python/ql/lib/change-notes/2026-01-02-prompt-injection.md
index e2bd4674bd57..22a00c64facc 100644
--- a/python/ql/lib/change-notes/2026-01-02-prompt-injection.md
+++ b/python/ql/lib/change-notes/2026-01-02-prompt-injection.md
@@ -1,5 +1,4 @@
---
category: minorAnalysis
---
-* Added propmpt injection query
* Added taint flow model and type model for `agents` and `openai` modules.
\ No newline at end of file
From 9ea0a1258c306751d047c94ddd90108915333d7a Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Tue, 6 Jan 2026 11:48:14 +0100
Subject: [PATCH 15/24] Fix capitalization typo
---
python/ql/src/Security/CWE-1427/examples/example.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/python/ql/src/Security/CWE-1427/examples/example.py b/python/ql/src/Security/CWE-1427/examples/example.py
index 7319513b53fd..a049f727b37a 100644
--- a/python/ql/src/Security/CWE-1427/examples/example.py
+++ b/python/ql/src/Security/CWE-1427/examples/example.py
@@ -1,11 +1,12 @@
from flask import Flask, request
-from agents import Agent, Runner
+from agents import Agent
from guardrails import GuardrailAgent
@app.route("/parameter-route")
def get_input():
input = request.args.get("input")
+ goodAgent = GuardrailAgent( # GOOD: Agent created with guardrails automatically configured.
config=Path("guardrails_config.json"),
name="Assistant",
instructions="This prompt is customized for " + input)
From fd8e1700c252249f2470183badc4b92fea3e0780 Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Tue, 6 Jan 2026 11:50:41 +0100
Subject: [PATCH 16/24] QLdoc
---
python/ql/lib/semmle/python/Concepts.qll | 1 +
1 file changed, 1 insertion(+)
diff --git a/python/ql/lib/semmle/python/Concepts.qll b/python/ql/lib/semmle/python/Concepts.qll
index 1fc51a06d59b..73d3b0d1e80f 100644
--- a/python/ql/lib/semmle/python/Concepts.qll
+++ b/python/ql/lib/semmle/python/Concepts.qll
@@ -345,6 +345,7 @@ module AIPrompt {
* extend `AIPrompt` instead.
*/
abstract class Range extends DataFlow::Node {
+ /** Gets an input that is used as AI prompt. */
abstract DataFlow::Node getAPrompt();
}
}
From 1a0feb4bac33d6e112d54f6ac8e4c670d083a58a Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Wed, 7 Jan 2026 21:55:58 +0100
Subject: [PATCH 17/24] precise models for experimental query
---
.../2026-01-02-prompt-injection.md | 1 +
.../semmle/python/frameworks/openai.model.yml | 4 +-
.../PromptInjectionCustomizations.qll | 48 ++++++++++++++
.../Security/CWE-1427/PromptInjection.qhelp | 0
.../Security/CWE-1427/PromptInjection.ql | 20 ++++++
.../Security/CWE-1427/examples/example.py | 0
.../PromptInjection.expected | 64 +++++++++----------
.../PromptInjection.qlref | 2 +
.../agent_instructions.py | 0
.../CWE-1427-PromptInjection/openai_test.py | 39 ++++++-----
10 files changed, 128 insertions(+), 50 deletions(-)
rename python/ql/src/{ => experimental}/Security/CWE-1427/PromptInjection.qhelp (100%)
create mode 100644 python/ql/src/experimental/Security/CWE-1427/PromptInjection.ql
rename python/ql/src/{ => experimental}/Security/CWE-1427/examples/example.py (100%)
rename python/ql/test/{ => experimental}/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected (62%)
create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
rename python/ql/test/{ => experimental}/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py (100%)
rename python/ql/test/{ => experimental}/query-tests/Security/CWE-1427-PromptInjection/openai_test.py (65%)
diff --git a/python/ql/lib/change-notes/2026-01-02-prompt-injection.md b/python/ql/lib/change-notes/2026-01-02-prompt-injection.md
index 22a00c64facc..21f04216ecbc 100644
--- a/python/ql/lib/change-notes/2026-01-02-prompt-injection.md
+++ b/python/ql/lib/change-notes/2026-01-02-prompt-injection.md
@@ -1,4 +1,5 @@
---
category: minorAnalysis
---
+* Added experimental query `py/prompt-injection` to detect potential prompt injection vulnerabilities in code using LLMs.
* Added taint flow model and type model for `agents` and `openai` modules.
\ No newline at end of file
diff --git a/python/ql/lib/semmle/python/frameworks/openai.model.yml b/python/ql/lib/semmle/python/frameworks/openai.model.yml
index 0f4f15677673..245d390ab8eb 100644
--- a/python/ql/lib/semmle/python/frameworks/openai.model.yml
+++ b/python/ql/lib/semmle/python/frameworks/openai.model.yml
@@ -3,9 +3,7 @@ extensions:
pack: codeql/python-all
extensible: sinkModel
data:
- - ['OpenAI', 'Member[responses].Member[create].Argument[input:,instructions:]', 'prompt-injection']
- - ['OpenAI', 'Member[realtime].Member[connect].ReturnValue.Member[conversation].Member[item].Member[create].Argument[item:]', 'prompt-injection']
- - ['OpenAI', 'Member[chat].Member[completions].Member[create].Argument[messages:]', 'prompt-injection']
+ - ['OpenAI', 'Member[beta].Member[assistants].Member[create].Argument[instructions:]', 'prompt-injection']
- addsTo:
pack: codeql/python-all
diff --git a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
index 5ea14283026b..880bf96e12fb 100644
--- a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
+++ b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
@@ -10,6 +10,7 @@ private import semmle.python.Concepts
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.dataflow.new.BarrierGuards
private import semmle.python.frameworks.data.ModelsAsData
+private import semmle.python.ApiGraphs
/**
* Provides default sources, sinks and sanitizers for detecting
@@ -48,6 +49,53 @@ module PromptInjection {
SinkFromModel() { this = ModelOutput::getASinkNode("prompt-injection").asSink() }
}
+ private class PromptContentSink extends Sink {
+ PromptContentSink() {
+ exists(API::Node openai, API::Node content |
+ openai =
+ API::moduleImport("openai")
+ .getMember(["OpenAI", "AsyncOpenAI", "AzureOpenAI"])
+ .getReturn() and
+ content =
+ [
+ openai
+ .getMember("responses")
+ .getMember("create")
+ .getKeywordParameter(["input", "instructions"]),
+ openai
+ .getMember("responses")
+ .getMember("create")
+ .getKeywordParameter(["input", "instructions"])
+ .getASubscript()
+ .getSubscript("content"),
+ openai
+ .getMember("realtime")
+ .getMember("connect")
+ .getReturn()
+ .getMember("conversation")
+ .getMember("item")
+ .getMember("create")
+ .getKeywordParameter("item")
+ .getSubscript("content"),
+ openai
+ .getMember("chat")
+ .getMember("completions")
+ .getMember("create")
+ .getKeywordParameter("messages")
+ .getASubscript()
+ .getSubscript("content")
+ ]
+ |
+ // content
+ if not exists(content.getASubscript())
+ then this = content.asSink()
+ else
+ // content.text
+ this = content.getASubscript().getSubscript("text").asSink()
+ )
+ }
+ }
+
/**
* A comparison with a constant, considered as a sanitizer-guard.
*/
diff --git a/python/ql/src/Security/CWE-1427/PromptInjection.qhelp b/python/ql/src/experimental/Security/CWE-1427/PromptInjection.qhelp
similarity index 100%
rename from python/ql/src/Security/CWE-1427/PromptInjection.qhelp
rename to python/ql/src/experimental/Security/CWE-1427/PromptInjection.qhelp
diff --git a/python/ql/src/experimental/Security/CWE-1427/PromptInjection.ql b/python/ql/src/experimental/Security/CWE-1427/PromptInjection.ql
new file mode 100644
index 000000000000..3bb985264fac
--- /dev/null
+++ b/python/ql/src/experimental/Security/CWE-1427/PromptInjection.ql
@@ -0,0 +1,20 @@
+/**
+ * @name Prompt injection
+ * @kind path-problem
+ * @problem.severity error
+ * @security-severity 5.0
+ * @precision high
+ * @id py/prompt-injection
+ * @tags security
+ * experimental
+ * external/cwe/cwe-1427
+ */
+
+import python
+import semmle.python.security.dataflow.PromptInjectionQuery
+import PromptInjectionFlow::PathGraph
+
+from PromptInjectionFlow::PathNode source, PromptInjectionFlow::PathNode sink
+where PromptInjectionFlow::flowPath(source, sink)
+select sink.getNode(), source, sink, "This prompt construction depends on a $@.", source.getNode(),
+ "user-provided value"
diff --git a/python/ql/src/Security/CWE-1427/examples/example.py b/python/ql/src/experimental/Security/CWE-1427/examples/example.py
similarity index 100%
rename from python/ql/src/Security/CWE-1427/examples/example.py
rename to python/ql/src/experimental/Security/CWE-1427/examples/example.py
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
similarity index 62%
rename from python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
rename to python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
index 6742141dfb0f..f52061f3c986 100644
--- a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
+++ b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
@@ -3,12 +3,17 @@
| openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
| openai_test.py:18:15:18:19 | ControlFlowNode for query | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:18:15:18:19 | ControlFlowNode for query | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
| openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
-| openai_test.py:23:15:37:9 | ControlFlowNode for List | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:23:15:37:9 | ControlFlowNode for List | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:26:28:26:51 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:26:28:26:51 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:33:33:33:37 | ControlFlowNode for query | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:33:33:33:37 | ControlFlowNode for query | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
| openai_test.py:41:22:41:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:41:22:41:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
| openai_test.py:42:15:42:19 | ControlFlowNode for query | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:42:15:42:19 | ControlFlowNode for query | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
-| openai_test.py:47:18:55:13 | ControlFlowNode for Dict | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:47:18:55:13 | ControlFlowNode for Dict | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
-| openai_test.py:59:18:70:9 | ControlFlowNode for List | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:59:18:70:9 | ControlFlowNode for List | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
-| openai_test.py:74:18:83:9 | ControlFlowNode for List | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:74:18:83:9 | ControlFlowNode for List | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:53:33:53:37 | ControlFlowNode for query | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:53:33:53:37 | ControlFlowNode for query | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:63:28:63:51 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:63:28:63:51 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:67:28:67:32 | ControlFlowNode for query | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:67:28:67:32 | ControlFlowNode for query | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:71:28:71:32 | ControlFlowNode for query | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:71:28:71:32 | ControlFlowNode for query | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:80:28:80:51 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:80:28:80:51 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:84:28:84:32 | ControlFlowNode for query | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:84:28:84:32 | ControlFlowNode for query | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| openai_test.py:92:22:92:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:92:22:92:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
edges
| agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | agent_instructions.py:2:26:2:32 | ControlFlowNode for request | provenance | |
| agent_instructions.py:2:26:2:32 | ControlFlowNode for request | agent_instructions.py:7:13:7:19 | ControlFlowNode for request | provenance | |
@@ -19,33 +24,27 @@ edges
| openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:2:26:2:32 | ControlFlowNode for request | provenance | |
| openai_test.py:2:26:2:32 | ControlFlowNode for request | openai_test.py:12:15:12:21 | ControlFlowNode for request | provenance | |
| openai_test.py:2:26:2:32 | ControlFlowNode for request | openai_test.py:13:13:13:19 | ControlFlowNode for request | provenance | |
-| openai_test.py:2:26:2:32 | ControlFlowNode for request | openai_test.py:14:12:14:18 | ControlFlowNode for request | provenance | |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | provenance | Sink:MaD:58613 |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | provenance | Sink:MaD:58613 |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:23:15:37:9 | ControlFlowNode for List | provenance | Sink:MaD:58613 |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:41:22:41:46 | ControlFlowNode for BinaryExpr | provenance | Sink:MaD:58613 |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:59:18:70:9 | ControlFlowNode for List | provenance | Sink:MaD:58615 |
-| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:74:18:83:9 | ControlFlowNode for List | provenance | Sink:MaD:58615 |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:26:28:26:51 | ControlFlowNode for BinaryExpr | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:41:22:41:46 | ControlFlowNode for BinaryExpr | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:63:28:63:51 | ControlFlowNode for BinaryExpr | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:80:28:80:51 | ControlFlowNode for BinaryExpr | provenance | |
+| openai_test.py:12:5:12:11 | ControlFlowNode for persona | openai_test.py:92:22:92:46 | ControlFlowNode for BinaryExpr | provenance | Sink:MaD:58613 |
| openai_test.py:12:15:12:21 | ControlFlowNode for request | openai_test.py:12:15:12:26 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:12:15:12:21 | ControlFlowNode for request | openai_test.py:13:13:13:24 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
-| openai_test.py:12:15:12:21 | ControlFlowNode for request | openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:12:15:12:26 | ControlFlowNode for Attribute | openai_test.py:12:15:12:41 | ControlFlowNode for Attribute() | provenance | dict.get |
| openai_test.py:12:15:12:41 | ControlFlowNode for Attribute() | openai_test.py:12:5:12:11 | ControlFlowNode for persona | provenance | |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:18:15:18:19 | ControlFlowNode for query | provenance | Sink:MaD:58613 |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:23:15:37:9 | ControlFlowNode for List | provenance | Sink:MaD:58613 |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:42:15:42:19 | ControlFlowNode for query | provenance | Sink:MaD:58613 |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:47:18:55:13 | ControlFlowNode for Dict | provenance | Sink:MaD:58614 |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:59:18:70:9 | ControlFlowNode for List | provenance | Sink:MaD:58615 |
-| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:74:18:83:9 | ControlFlowNode for List | provenance | Sink:MaD:58615 |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:18:15:18:19 | ControlFlowNode for query | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:33:33:33:37 | ControlFlowNode for query | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:42:15:42:19 | ControlFlowNode for query | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:53:33:53:37 | ControlFlowNode for query | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:67:28:67:32 | ControlFlowNode for query | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:71:28:71:32 | ControlFlowNode for query | provenance | |
+| openai_test.py:13:5:13:9 | ControlFlowNode for query | openai_test.py:84:28:84:32 | ControlFlowNode for query | provenance | |
| openai_test.py:13:13:13:19 | ControlFlowNode for request | openai_test.py:13:13:13:24 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
-| openai_test.py:13:13:13:19 | ControlFlowNode for request | openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| openai_test.py:13:13:13:24 | ControlFlowNode for Attribute | openai_test.py:13:13:13:37 | ControlFlowNode for Attribute() | provenance | dict.get |
| openai_test.py:13:13:13:37 | ControlFlowNode for Attribute() | openai_test.py:13:5:13:9 | ControlFlowNode for query | provenance | |
-| openai_test.py:14:5:14:8 | ControlFlowNode for role | openai_test.py:47:18:55:13 | ControlFlowNode for Dict | provenance | Sink:MaD:58614 |
-| openai_test.py:14:5:14:8 | ControlFlowNode for role | openai_test.py:59:18:70:9 | ControlFlowNode for List | provenance | Sink:MaD:58615 |
-| openai_test.py:14:12:14:18 | ControlFlowNode for request | openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
-| openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | openai_test.py:14:12:14:35 | ControlFlowNode for Attribute() | provenance | dict.get |
-| openai_test.py:14:12:14:35 | ControlFlowNode for Attribute() | openai_test.py:14:5:14:8 | ControlFlowNode for role | provenance | |
nodes
| agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
| agent_instructions.py:2:26:2:32 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
@@ -64,17 +63,18 @@ nodes
| openai_test.py:13:13:13:19 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
| openai_test.py:13:13:13:24 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
| openai_test.py:13:13:13:37 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
-| openai_test.py:14:5:14:8 | ControlFlowNode for role | semmle.label | ControlFlowNode for role |
-| openai_test.py:14:12:14:18 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| openai_test.py:14:12:14:23 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| openai_test.py:14:12:14:35 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
| openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| openai_test.py:18:15:18:19 | ControlFlowNode for query | semmle.label | ControlFlowNode for query |
| openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
-| openai_test.py:23:15:37:9 | ControlFlowNode for List | semmle.label | ControlFlowNode for List |
+| openai_test.py:26:28:26:51 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
+| openai_test.py:33:33:33:37 | ControlFlowNode for query | semmle.label | ControlFlowNode for query |
| openai_test.py:41:22:41:46 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| openai_test.py:42:15:42:19 | ControlFlowNode for query | semmle.label | ControlFlowNode for query |
-| openai_test.py:47:18:55:13 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
-| openai_test.py:59:18:70:9 | ControlFlowNode for List | semmle.label | ControlFlowNode for List |
-| openai_test.py:74:18:83:9 | ControlFlowNode for List | semmle.label | ControlFlowNode for List |
+| openai_test.py:53:33:53:37 | ControlFlowNode for query | semmle.label | ControlFlowNode for query |
+| openai_test.py:63:28:63:51 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
+| openai_test.py:67:28:67:32 | ControlFlowNode for query | semmle.label | ControlFlowNode for query |
+| openai_test.py:71:28:71:32 | ControlFlowNode for query | semmle.label | ControlFlowNode for query |
+| openai_test.py:80:28:80:51 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
+| openai_test.py:84:28:84:32 | ControlFlowNode for query | semmle.label | ControlFlowNode for query |
+| openai_test.py:92:22:92:46 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
subpaths
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
new file mode 100644
index 000000000000..04d00b1b4b8c
--- /dev/null
+++ b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
@@ -0,0 +1,2 @@
+query: Security/CWE-1427/PromptInjection.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
similarity index 100%
rename from python/ql/test/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
rename to python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/openai_test.py b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
similarity index 65%
rename from python/ql/test/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
rename to python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
index f5b29a25dd56..ae4c04633129 100644
--- a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
+++ b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
@@ -23,18 +23,18 @@ def get_input_openai():
input=[
{
"role": "developer",
- "content": "Talk like a " + persona
+ "content": "Talk like a " + persona # $ Alert[py/prompt-injection]
},
{
"role": "user",
"content": [
{
"type": "input_text",
- "text": query
+ "text": query # $ Alert[py/prompt-injection]
}
]
}
- ] # $ Alert[py/prompt-injection]
+ ]
)
response3 = await async_client.responses.create(
@@ -48,37 +48,46 @@ def get_input_openai():
"type": "message",
"role": role,
"content": [
- {"type": "input_text",
- "text": query
- }
+ {
+ "type": "input_text",
+ "text": query # $ Alert[py/prompt-injection]
+ }
],
- } # $ Alert[py/prompt-injection]
+ }
)
completion1 = client.chat.completions.create(
messages=[
- {"role": "developer",
- "content": "Talk like a " + persona},
+ {
+ "role": "developer",
+ "content": "Talk like a " + persona # $ Alert[py/prompt-injection]
+ },
{
"role": "user",
- "content": query,
+ "content": query, # $ Alert[py/prompt-injection]
},
{
"role": role,
- "content": query,
+ "content": query, # $ Alert[py/prompt-injection]
}
- ] # $ Alert[py/prompt-injection]
+ ]
)
completion2 = azure_client.chat.completions.create(
messages=[
{
"role": "developer",
- "content": "Talk like a " + persona
+ "content": "Talk like a " + persona # $ Alert[py/prompt-injection]
},
{
"role": "user",
- "content": query,
+ "content": query, # $ Alert[py/prompt-injection]
}
- ] # $ Alert[py/prompt-injection]
+ ]
+ )
+
+ assistant = client.beta.assistants.create(
+ name="Test Agent",
+ model="gpt-4.1",
+ instructions="Talks like a " + persona # $ Alert[py/prompt-injection]
)
From 01b9fa245388341c70a6e4d731f21d94d1fa3d5b Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Wed, 7 Jan 2026 21:59:12 +0100
Subject: [PATCH 18/24] removed spurious file
---
.../src/Security/CWE-1427/PromptInjection.ql | 20 -------------------
1 file changed, 20 deletions(-)
delete mode 100644 python/ql/src/Security/CWE-1427/PromptInjection.ql
diff --git a/python/ql/src/Security/CWE-1427/PromptInjection.ql b/python/ql/src/Security/CWE-1427/PromptInjection.ql
deleted file mode 100644
index 4635b058959f..000000000000
--- a/python/ql/src/Security/CWE-1427/PromptInjection.ql
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * @name Prompt injection
- * @description User input used in developer message and or system prompt can allow for Prompt Injection attacks.
- * @kind path-problem
- * @problem.severity error
- * @security-severity 5.0
- * @precision high
- * @id py/prompt-injection
- * @tags security
- * external/cwe/cwe-1427
- */
-
-import python
-import semmle.python.security.dataflow.PromptInjectionQuery
-import PromptInjectionFlow::PathGraph
-
-from PromptInjectionFlow::PathNode source, PromptInjectionFlow::PathNode sink
-where PromptInjectionFlow::flowPath(source, sink)
-select sink.getNode(), source, sink, "This prompt construction depends on a $@.", source.getNode(),
- "user-provided value"
From 29aad2e5164accba59bcc5900fdcdeb2ebd20672 Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Wed, 7 Jan 2026 22:00:16 +0100
Subject: [PATCH 19/24] remove test
---
.../Security/CWE-1427-PromptInjection/PromptInjection.qlref | 2 --
1 file changed, 2 deletions(-)
delete mode 100644 python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
diff --git a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref b/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
deleted file mode 100644
index c01f22bf45b8..000000000000
--- a/python/ql/test/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
+++ /dev/null
@@ -1,2 +0,0 @@
-query: Security/CWE-1427/PromptInjection.ql
-postprocess: utils/test/InlineExpectationsTestQuery.ql
From 0a36be1ae34ac25ae43819682c6cb4c9e7f1117f Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Wed, 7 Jan 2026 22:44:44 +0100
Subject: [PATCH 20/24] Refactor openai model
---
.../lib/semmle/python/frameworks/OpenAI.qll | 57 ++++++++++++++++---
.../PromptInjectionCustomizations.qll | 45 +--------------
2 files changed, 52 insertions(+), 50 deletions(-)
diff --git a/python/ql/lib/semmle/python/frameworks/OpenAI.qll b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
index 77a4787a5445..a247645081a4 100644
--- a/python/ql/lib/semmle/python/frameworks/OpenAI.qll
+++ b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
@@ -7,14 +7,57 @@ private import python
private import semmle.python.ApiGraphs
/**
- * Provides models for Agent (instances of the `agents.Agent` class).
+ * Provides models for Agent (instances of the `openai.OpenAI` class).
*
- * See https://github.com/openai/openai-agents-python.
+ * See https://github.com/openai/openai-python.
*/
-module Agent {
- /** Gets a reference to the `agents.Agent` class. */
- API::Node classRef() { result = API::moduleImport("agents").getMember("Agent") }
+module OpenAI {
+ /** Gets a reference to the `openai.OpenAI` class. */
+ API::Node classRef() {
+ result =
+ API::moduleImport("openai").getMember(["OpenAI", "AsyncOpenAI", "AzureOpenAI"]).getReturn()
+ }
- /** Gets a reference to a potential property of `agents.Agent` called instructions which refers to the system prompt. */
- API::Node sink() { result = classRef().getACall().getKeywordParameter("instructions") }
+ /** Gets a reference to a potential property of `openai.OpenAI` called instructions which refers to the system prompt. */
+ API::Node getContentNode() {
+ exists(API::Node content |
+ content =
+ classRef()
+ .getMember("responses")
+ .getMember("create")
+ .getKeywordParameter(["input", "instructions"]) or
+ content =
+ classRef()
+ .getMember("responses")
+ .getMember("create")
+ .getKeywordParameter(["input", "instructions"])
+ .getASubscript()
+ .getSubscript("content") or
+ content =
+ classRef()
+ .getMember("realtime")
+ .getMember("connect")
+ .getReturn()
+ .getMember("conversation")
+ .getMember("item")
+ .getMember("create")
+ .getKeywordParameter("item")
+ .getSubscript("content") or
+ content =
+ classRef()
+ .getMember("chat")
+ .getMember("completions")
+ .getMember("create")
+ .getKeywordParameter("messages")
+ .getASubscript()
+ .getSubscript("content")
+ |
+ // content
+ if not exists(content.getASubscript())
+ then result = content
+ else
+ // content.text
+ result = content.getASubscript().getSubscript("text")
+ )
+ }
}
diff --git a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
index 880bf96e12fb..c7f480d47b2e 100644
--- a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
+++ b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
@@ -10,7 +10,7 @@ private import semmle.python.Concepts
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.dataflow.new.BarrierGuards
private import semmle.python.frameworks.data.ModelsAsData
-private import semmle.python.ApiGraphs
+private import semmle.python.frameworks.OpenAI
/**
* Provides default sources, sinks and sanitizers for detecting
@@ -51,48 +51,7 @@ module PromptInjection {
private class PromptContentSink extends Sink {
PromptContentSink() {
- exists(API::Node openai, API::Node content |
- openai =
- API::moduleImport("openai")
- .getMember(["OpenAI", "AsyncOpenAI", "AzureOpenAI"])
- .getReturn() and
- content =
- [
- openai
- .getMember("responses")
- .getMember("create")
- .getKeywordParameter(["input", "instructions"]),
- openai
- .getMember("responses")
- .getMember("create")
- .getKeywordParameter(["input", "instructions"])
- .getASubscript()
- .getSubscript("content"),
- openai
- .getMember("realtime")
- .getMember("connect")
- .getReturn()
- .getMember("conversation")
- .getMember("item")
- .getMember("create")
- .getKeywordParameter("item")
- .getSubscript("content"),
- openai
- .getMember("chat")
- .getMember("completions")
- .getMember("create")
- .getKeywordParameter("messages")
- .getASubscript()
- .getSubscript("content")
- ]
- |
- // content
- if not exists(content.getASubscript())
- then this = content.asSink()
- else
- // content.text
- this = content.getASubscript().getSubscript("text").asSink()
- )
+ this = OpenAI::getContentNode().asSink()
}
}
From dccaa84b96e557e85a146ff1c9b6a7939a443f66 Mon Sep 17 00:00:00 2001
From: Kristen Newbury
Date: Fri, 9 Jan 2026 12:24:20 -0500
Subject: [PATCH 21/24] Improve agents sdk modelling (#5)
* Add testcase and coverage for agents sdk runner run with input param
* Rename agent sdk module for clarity
* Add case for unnamed param use in runner run from agent sdk
---
.../lib/semmle/python/frameworks/OpenAI.qll | 23 ++++++++++++++-
.../PromptInjectionCustomizations.qll | 2 ++
.../PromptInjection.expected | 14 ++++++++++
.../PromptInjection.qlref | 2 +-
.../agent_instructions.py | 28 ++++++++++++++++++-
5 files changed, 66 insertions(+), 3 deletions(-)
diff --git a/python/ql/lib/semmle/python/frameworks/OpenAI.qll b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
index a247645081a4..ccb8f1bb6b6c 100644
--- a/python/ql/lib/semmle/python/frameworks/OpenAI.qll
+++ b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
@@ -1,11 +1,32 @@
/**
- * Provides classes modeling security-relevant aspects of the `openAI`Agents SDK package.
+ * Provides classes modeling security-relevant aspects of the `openAI` Agents SDK package.
* See https://github.com/openai/openai-agents-python.
+ * As well as the regular openai python interface.
+ * See https://github.com/openai/openai-python.
*/
private import python
private import semmle.python.ApiGraphs
+/**
+ * Provides models for agents SDK (instances of the `agents.Runner` class etc).
+ *
+ * See https://github.com/openai/openai-agents-python.
+ */
+module AgentSDK {
+ /** Gets a reference to the `agents.Agent` class. */
+ API::Node classRef() { result = API::moduleImport("agents").getMember("Runner") }
+
+ API::Node runMembers() { result = classRef().getMember(["run", "run_sync", "run_streamed"]) }
+
+ /** Gets a reference to a potential property of `agents.Runner` called input which can refer to a system prompt depending on the role specified. */
+ API::Node getContentNode() {
+ result = runMembers().getKeywordParameter("input").getASubscript().getSubscript("content")
+ or
+ result = runMembers().getParameter(_).getASubscript().getSubscript("content")
+ }
+}
+
/**
* Provides models for Agent (instances of the `openai.OpenAI` class).
*
diff --git a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
index c7f480d47b2e..aaa8c05418ed 100644
--- a/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
+++ b/python/ql/lib/semmle/python/security/dataflow/PromptInjectionCustomizations.qll
@@ -52,6 +52,8 @@ module PromptInjection {
private class PromptContentSink extends Sink {
PromptContentSink() {
this = OpenAI::getContentNode().asSink()
+ or
+ this = AgentSDK::getContentNode().asSink()
}
}
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
index f52061f3c986..bbf9e5665bab 100644
--- a/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
+++ b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.expected
@@ -1,5 +1,7 @@
#select
| agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| agent_instructions.py:25:28:25:32 | ControlFlowNode for input | agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | agent_instructions.py:25:28:25:32 | ControlFlowNode for input | This prompt construction depends on a $@. | agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
+| agent_instructions.py:35:28:35:32 | ControlFlowNode for input | agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | agent_instructions.py:35:28:35:32 | ControlFlowNode for input | This prompt construction depends on a $@. | agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
| openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:17:22:17:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
| openai_test.py:18:15:18:19 | ControlFlowNode for query | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:18:15:18:19 | ControlFlowNode for query | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
| openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:22:22:22:46 | ControlFlowNode for BinaryExpr | This prompt construction depends on a $@. | openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
@@ -17,10 +19,16 @@
edges
| agent_instructions.py:2:26:2:32 | ControlFlowNode for ImportMember | agent_instructions.py:2:26:2:32 | ControlFlowNode for request | provenance | |
| agent_instructions.py:2:26:2:32 | ControlFlowNode for request | agent_instructions.py:7:13:7:19 | ControlFlowNode for request | provenance | |
+| agent_instructions.py:2:26:2:32 | ControlFlowNode for request | agent_instructions.py:17:13:17:19 | ControlFlowNode for request | provenance | |
| agent_instructions.py:7:5:7:9 | ControlFlowNode for input | agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | provenance | Sink:MaD:93 |
| agent_instructions.py:7:13:7:19 | ControlFlowNode for request | agent_instructions.py:7:13:7:24 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
| agent_instructions.py:7:13:7:24 | ControlFlowNode for Attribute | agent_instructions.py:7:13:7:37 | ControlFlowNode for Attribute() | provenance | dict.get |
| agent_instructions.py:7:13:7:37 | ControlFlowNode for Attribute() | agent_instructions.py:7:5:7:9 | ControlFlowNode for input | provenance | |
+| agent_instructions.py:17:5:17:9 | ControlFlowNode for input | agent_instructions.py:25:28:25:32 | ControlFlowNode for input | provenance | |
+| agent_instructions.py:17:5:17:9 | ControlFlowNode for input | agent_instructions.py:35:28:35:32 | ControlFlowNode for input | provenance | |
+| agent_instructions.py:17:13:17:19 | ControlFlowNode for request | agent_instructions.py:17:13:17:24 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
+| agent_instructions.py:17:13:17:24 | ControlFlowNode for Attribute | agent_instructions.py:17:13:17:37 | ControlFlowNode for Attribute() | provenance | dict.get |
+| agent_instructions.py:17:13:17:37 | ControlFlowNode for Attribute() | agent_instructions.py:17:5:17:9 | ControlFlowNode for input | provenance | |
| openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | openai_test.py:2:26:2:32 | ControlFlowNode for request | provenance | |
| openai_test.py:2:26:2:32 | ControlFlowNode for request | openai_test.py:12:15:12:21 | ControlFlowNode for request | provenance | |
| openai_test.py:2:26:2:32 | ControlFlowNode for request | openai_test.py:13:13:13:19 | ControlFlowNode for request | provenance | |
@@ -53,6 +61,12 @@ nodes
| agent_instructions.py:7:13:7:24 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
| agent_instructions.py:7:13:7:37 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
| agent_instructions.py:9:50:9:89 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
+| agent_instructions.py:17:5:17:9 | ControlFlowNode for input | semmle.label | ControlFlowNode for input |
+| agent_instructions.py:17:13:17:19 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| agent_instructions.py:17:13:17:24 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| agent_instructions.py:17:13:17:37 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
+| agent_instructions.py:25:28:25:32 | ControlFlowNode for input | semmle.label | ControlFlowNode for input |
+| agent_instructions.py:35:28:35:32 | ControlFlowNode for input | semmle.label | ControlFlowNode for input |
| openai_test.py:2:26:2:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
| openai_test.py:2:26:2:32 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
| openai_test.py:12:5:12:11 | ControlFlowNode for persona | semmle.label | ControlFlowNode for persona |
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
index 04d00b1b4b8c..08466562ffe7 100644
--- a/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
+++ b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/PromptInjection.qlref
@@ -1,2 +1,2 @@
-query: Security/CWE-1427/PromptInjection.ql
+query: experimental/Security/CWE-1427/PromptInjection.ql
postprocess: utils/test/InlineExpectationsTestQuery.ql
\ No newline at end of file
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
index ebe26942ce43..12cebc1b5831 100644
--- a/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
+++ b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/agent_instructions.py
@@ -3,10 +3,36 @@
app = Flask(__name__)
@app.route("/parameter-route")
-def get_input():
+def get_input1():
input = request.args.get("input")
agent = Agent(name="Assistant", instructions="This prompt is customized for " + input) # $Alert[py/prompt-injection]
result = Runner.run_sync(agent, "This is a user message.")
print(result.final_output)
+
+
+@app.route("/parameter-route")
+def get_input2():
+ input = request.args.get("input")
+
+ agent = Agent(name="Assistant", instructions="This prompt is not customized.")
+ result = Runner.run_sync(
+ agent=agent,
+ input=[
+ {
+ "role": "user",
+ "content": input, # $Alert[py/prompt-injection]
+ }
+ ]
+ )
+
+ result2 = Runner.run_sync(
+ agent,
+ [
+ {
+ "role": "user",
+ "content": input, # $Alert[py/prompt-injection]
+ }
+ ]
+ )
From 1ec82d9f56878eb24a27aa0c2d700cfc7e78da70 Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Fri, 9 Jan 2026 18:27:41 +0100
Subject: [PATCH 22/24] Update OpenAI.qll
---
python/ql/lib/semmle/python/frameworks/OpenAI.qll | 1 +
1 file changed, 1 insertion(+)
diff --git a/python/ql/lib/semmle/python/frameworks/OpenAI.qll b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
index ccb8f1bb6b6c..601919967cf8 100644
--- a/python/ql/lib/semmle/python/frameworks/OpenAI.qll
+++ b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
@@ -17,6 +17,7 @@ module AgentSDK {
/** Gets a reference to the `agents.Agent` class. */
API::Node classRef() { result = API::moduleImport("agents").getMember("Runner") }
+ /** Gets a reference to the `run` members. */
API::Node runMembers() { result = classRef().getMember(["run", "run_sync", "run_streamed"]) }
/** Gets a reference to a potential property of `agents.Runner` called input which can refer to a system prompt depending on the role specified. */
From 16370d6cd1c38d11114f1ba6de9dd5b073dee7ec Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Fri, 9 Jan 2026 19:07:52 +0100
Subject: [PATCH 23/24] Update
python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../Security/CWE-1427-PromptInjection/openai_test.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/openai_test.py b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
index ae4c04633129..2b25609670c5 100644
--- a/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
+++ b/python/ql/test/experimental/query-tests/Security/CWE-1427-PromptInjection/openai_test.py
@@ -8,7 +8,7 @@
@app.route("/openai")
-def get_input_openai():
+async def get_input_openai():
persona = request.args.get("persona")
query = request.args.get("query")
role = request.args.get("role")
From 454268187f87928e1e261648d7e8ea60f59859ea Mon Sep 17 00:00:00 2001
From: Mauro Baluda
Date: Fri, 9 Jan 2026 19:11:41 +0100
Subject: [PATCH 24/24] Update
python/ql/lib/semmle/python/frameworks/OpenAI.qll
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
python/ql/lib/semmle/python/frameworks/OpenAI.qll | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/ql/lib/semmle/python/frameworks/OpenAI.qll b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
index 601919967cf8..7cd11ebabefe 100644
--- a/python/ql/lib/semmle/python/frameworks/OpenAI.qll
+++ b/python/ql/lib/semmle/python/frameworks/OpenAI.qll
@@ -14,7 +14,7 @@ private import semmle.python.ApiGraphs
* See https://github.com/openai/openai-agents-python.
*/
module AgentSDK {
- /** Gets a reference to the `agents.Agent` class. */
+ /** Gets a reference to the `agents.Runner` class. */
API::Node classRef() { result = API::moduleImport("agents").getMember("Runner") }
/** Gets a reference to the `run` members. */