Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
005db5b
Add first version prompt injection query python openai agents sdk
knewbury01 Dec 12, 2025
7a9e03d
Add support for `openai.OpenAI` client library
mbaluda Jan 2, 2026
b30444b
Merge branch 'knewbury01/add-prompt-injection-query-python' into prom…
mbaluda Jan 2, 2026
6c5c87e
Fix projcet build errors
mbaluda Jan 2, 2026
616698c
Fix newline at end of PromptInjection.qlref
mbaluda Jan 2, 2026
942834d
Update python/ql/lib/semmle/python/frameworks/OpenAI.qll
mbaluda Jan 2, 2026
df979da
Update python/ql/src/Security/CWE-1427/PromptInjection.ql
mbaluda Jan 2, 2026
bacecb7
Add example to qlhelp
mbaluda Jan 2, 2026
a9d0a16
Fix missing predicate
mbaluda Jan 2, 2026
04193f4
Une inline expectations
mbaluda Jan 5, 2026
2c83dc3
Use models as data
mbaluda Jan 5, 2026
0c7996e
Update python/ql/src/Security/CWE-1427/examples/example.py
mbaluda Jan 6, 2026
21a2146
Update python/ql/lib/semmle/python/Concepts.qll
mbaluda Jan 6, 2026
7d450c5
Update python/ql/src/Security/CWE-1427/PromptInjection.qhelp
mbaluda Jan 6, 2026
c352ffd
Update python/ql/lib/change-notes/2026-01-02-prompt-injection.md
mbaluda Jan 6, 2026
9ea0a12
Fix capitalization typo
mbaluda Jan 6, 2026
fd8e170
QLdoc
mbaluda Jan 6, 2026
b4275e8
Merge branch 'main' into knewbury01/add-prompt-injection-query-python
knewbury01 Jan 7, 2026
4117252
Merge pull request #4 from github/main
mbaluda Jan 7, 2026
c7d99a1
Merge branch 'knewbury01/add-prompt-injection-query-python' into prom…
mbaluda Jan 7, 2026
1a0feb4
precise models for experimental query
mbaluda Jan 7, 2026
01b9fa2
removed spurious file
mbaluda Jan 7, 2026
29aad2e
remove test
mbaluda Jan 7, 2026
0a36be1
Refactor openai model
mbaluda Jan 7, 2026
dccaa84
Improve agents sdk modelling (#5)
knewbury01 Jan 9, 2026
1ec82d9
Update OpenAI.qll
mbaluda Jan 9, 2026
3c14266
Merge branch 'github:main' into prompt-injection
mbaluda Jan 9, 2026
16370d6
Update python/ql/test/experimental/query-tests/Security/CWE-1427-Prom…
mbaluda Jan 9, 2026
4542681
Update python/ql/lib/semmle/python/frameworks/OpenAI.qll
mbaluda Jan 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions python/ql/lib/change-notes/2026-01-02-prompt-injection.md
Original file line number Diff line number Diff line change
@@ -0,0 +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.
25 changes: 25 additions & 0 deletions python/ql/lib/semmle/python/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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 used as AI prompt. */
abstract DataFlow::Node getAPrompt();
}
}

/**
* A data-flow node that logs data.
*
Expand Down
1 change: 1 addition & 0 deletions python/ql/lib/semmle/python/Frameworks.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 85 additions & 0 deletions python/ql/lib/semmle/python/frameworks/OpenAI.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* 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.
Comment on lines +2 to +4
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment refers to "openAI" with inconsistent capitalization. The official product name is "OpenAI" (capital O and capital AI). The comment should be updated for consistency with the official branding.

This issue also appears in the following locations of the same file:

  • line 4
Suggested change
* 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.
* 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.

Copilot uses AI. Check for mistakes.
* 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 {

Check warning

Code scanning / CodeQL

Acronyms should be PascalCase/camelCase Warning

Acronyms in AgentSDK should be PascalCase/camelCase.
/** Gets a reference to the `agents.Runner` 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. */
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).
*
* See https://github.com/openai/openai-python.
*/
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 `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")
)
}
}
6 changes: 6 additions & 0 deletions python/ql/lib/semmle/python/frameworks/agent.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
extensions:
- addsTo:
pack: codeql/python-all
extensible: sinkModel
data:
- ['agents', 'Member[Agent].Argument[instructions:]', 'prompt-injection']
12 changes: 12 additions & 0 deletions python/ql/lib/semmle/python/frameworks/openai.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
extensions:
- addsTo:
pack: codeql/python-all
extensible: sinkModel
data:
- ['OpenAI', 'Member[beta].Member[assistants].Member[create].Argument[instructions:]', 'prompt-injection']

- addsTo:
pack: codeql/python-all
extensible: typeModel
data:
- ['OpenAI', 'openai', 'Member[OpenAI,AsyncOpenAI,AzureOpenAI].ReturnValue']
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* 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
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.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 { }

/**
* A prompt to an AI model, considered as a flow sink.
*/
class AIPromptAsSink extends Sink {
AIPromptAsSink() { this = any(AIPrompt p).getAPrompt() }
}

private class SinkFromModel extends Sink {
SinkFromModel() { this = ModelOutput::getASinkNode("prompt-injection").asSink() }
}

private class PromptContentSink extends Sink {
PromptContentSink() {
this = OpenAI::getContentNode().asSink()
or
this = AgentSDK::getContentNode().asSink()
}
}

/**
* A comparison with a constant, considered as a sanitizer-guard.
*/
class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Provides a taint-tracking configuration 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
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 }

predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }

predicate observeDiffInformedIncrementalMode() { any() }
}

/** Global taint-tracking for detecting "prompt injection" vulnerabilities. */
module PromptInjectionFlow = TaintTracking::Global<PromptInjectionConfig>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>

<overview>
<p>Prompts can be constructed to bypass the original purposes of an agent and lead to sensitive data leak or
operations that were not intended.</p>
</overview>

<recommendation>
<p>Sanitize user input and also avoid using user input in developer or system level prompts.</p>
</recommendation>

<example>
<p>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.</p>
<sample src="examples/example.py" />
</example>

<references>
<li>OpenAI: <a href="https://openai.github.io/openai-guardrails-python">Guardrails</a>.</li>
</references>

</qhelp>
20 changes: 20 additions & 0 deletions python/ql/src/experimental/Security/CWE-1427/PromptInjection.ql
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from flask import Flask, request
from agents import Agent
from guardrails import GuardrailAgent

Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable app is used but not defined. The variable should be defined before being used, similar to the test files where app = Flask(__name__) is declared.

Suggested change
app = Flask(__name__)

Copilot uses AI. Check for mistakes.
@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.
)
Loading