-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Python: Prompt injection in OpenAI clients #21141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
005db5b
7a9e03d
b30444b
6c5c87e
616698c
942834d
df979da
bacecb7
a9d0a16
04193f4
2c83dc3
0c7996e
21a2146
7d450c5
c352ffd
9ea0a12
fd8e170
b4275e8
4117252
c7d99a1
1a0feb4
01b9fa2
29aad2e
0a36be1
dccaa84
1ec82d9
3c14266
16370d6
4542681
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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. | ||
| * 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.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") | ||
| ) | ||
| } | ||
| } | ||
| 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'] |
| 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 | ||
mbaluda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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> | ||
| 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 | ||||||||
|
|
||||||||
|
||||||||
| app = Flask(__name__) |
There was a problem hiding this comment.
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: