Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
db493ef
Python: port prompt injection queries (system + user) from JS PR #21953
BazookaMusic Jun 18, 2026
72bc52b
Python: promote prompt injection queries from experimental to production
BazookaMusic Jun 18, 2026
8e5f214
Fix OpenRouter Python API and expand model coverage
BazookaMusic Jun 18, 2026
018ba92
Add additional Python prompt-injection sinks for uncovered SDK methods
BazookaMusic Jun 18, 2026
4fbb02d
Merge branch 'main' into bazookamusic/python-prompt-injection
BazookaMusic Jun 29, 2026
0abd325
Python: stabilize XsltInjection test MaD index via PrettyPrintModels
BazookaMusic Jun 29, 2026
a06f22a
Python: apply codeql query format to prompt-injection framework files
BazookaMusic Jun 29, 2026
56614cb
Python: remove redundant DataFlow import in SystemPromptInjectionCust…
BazookaMusic Jun 29, 2026
4c965e7
Merge branch 'main' into bazookamusic/python-prompt-injection
BazookaMusic Jul 2, 2026
27d2a2b
Fix google genai models
BazookaMusic Jul 2, 2026
2eb2d62
Add proper coverage of tools
BazookaMusic Jul 2, 2026
49e0d6d
Delete summary
BazookaMusic Jul 2, 2026
2bee6b8
Merge branch 'bazookamusic/python-prompt-injection' of https://github…
BazookaMusic Jul 2, 2026
3948f3f
Note tool-description coverage in prompt-injection change note
BazookaMusic Jul 2, 2026
15df1f3
Simpler change note
BazookaMusic Jul 2, 2026
f0b6414
Merge branch 'main' into bazookamusic/python-prompt-injection
BazookaMusic Jul 3, 2026
8d3071d
Add the missing alerts
BazookaMusic Jul 6, 2026
48b841c
adjust model for realtime
BazookaMusic Jul 7, 2026
e1d8d27
Merge branch 'main' into bazookamusic/python-prompt-injection
BazookaMusic Jul 16, 2026
c72c7d5
Merge main into Python prompt-injection branch
BazookaMusic Nov 5, 2026
041d873
Python: route prompt-injection models through the AIPrompt concept
BazookaMusic Nov 5, 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
6 changes: 6 additions & 0 deletions docs/codeql/reusables/supported-frameworks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,12 @@ and the CodeQL library pack ``codeql/python-all`` (`changelog <https://github.co
libxml2, XML processing library
lxml, XML processing library
xmltodict, XML processing library
anthropic, AI model SDK
google-genai, AI model SDK
langchain, AI framework
openai, AI model SDK
openai-agents, AI agent framework
openrouter, AI model SDK


Ruby built-in support
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ ql/python/ql/src/Metrics/NumberOfStatements.ql
ql/python/ql/src/Metrics/TransitiveImports.ql
ql/python/ql/src/Security/CWE-020-ExternalAPIs/ExternalAPIsUsedWithUntrustedData.ql
ql/python/ql/src/Security/CWE-020-ExternalAPIs/UntrustedDataToExternalAPI.ql
ql/python/ql/src/Security/CWE-1427/UserPromptInjection.ql
ql/python/ql/src/Security/CWE-798/HardcodedCredentials.ql
ql/python/ql/src/Statements/C_StyleParentheses.ql
ql/python/ql/src/Statements/DocStrings.ql
Expand Down Expand Up @@ -87,7 +88,6 @@ ql/python/ql/src/experimental/Security/CWE-079/EmailXss.ql
ql/python/ql/src/experimental/Security/CWE-091/XsltInjection.ql
ql/python/ql/src/experimental/Security/CWE-094/Js2Py.ql
ql/python/ql/src/experimental/Security/CWE-1236/CsvInjection.ql
ql/python/ql/src/experimental/Security/CWE-1427/PromptInjection.ql
ql/python/ql/src/experimental/Security/CWE-176/UnicodeBypassValidation.ql
ql/python/ql/src/experimental/Security/CWE-208/TimingAttackAgainstHash/PossibleTimingAttackAgainstHash.ql
ql/python/ql/src/experimental/Security/CWE-208/TimingAttackAgainstHash/TimingAttackAgainstHash.ql
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ql/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
ql/python/ql/src/Security/CWE-113/HeaderInjection.ql
ql/python/ql/src/Security/CWE-116/BadTagFilter.ql
ql/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
ql/python/ql/src/Security/CWE-1427/SystemPromptInjection.ql
ql/python/ql/src/Security/CWE-209/StackTraceExposure.ql
ql/python/ql/src/Security/CWE-215/FlaskDebug.ql
ql/python/ql/src/Security/CWE-285/PamAuthorization.ql
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ ql/python/ql/src/Security/CWE-113/HeaderInjection.ql
ql/python/ql/src/Security/CWE-116/BadTagFilter.ql
ql/python/ql/src/Security/CWE-117/LogInjection.ql
ql/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
ql/python/ql/src/Security/CWE-1427/SystemPromptInjection.ql
ql/python/ql/src/Security/CWE-209/StackTraceExposure.ql
ql/python/ql/src/Security/CWE-215/FlaskDebug.ql
ql/python/ql/src/Security/CWE-285/PamAuthorization.ql
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ ql/python/ql/src/Security/CWE-113/HeaderInjection.ql
ql/python/ql/src/Security/CWE-116/BadTagFilter.ql
ql/python/ql/src/Security/CWE-117/LogInjection.ql
ql/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
ql/python/ql/src/Security/CWE-1427/SystemPromptInjection.ql
ql/python/ql/src/Security/CWE-209/StackTraceExposure.ql
ql/python/ql/src/Security/CWE-215/FlaskDebug.ql
ql/python/ql/src/Security/CWE-285/PamAuthorization.ql
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: feature
---
* Added the `AIPrompt` concept to model AI prompts and distinguish system-level prompts from user-level prompts. Added prompt-injection sink models (`system-prompt-injection` and `user-prompt-injection` kinds) for the `openai`, OpenAI Agents SDK (`agents`), `anthropic`, `google-genai`, `openrouter`, and `langchain` frameworks.
37 changes: 37 additions & 0 deletions python/ql/lib/semmle/python/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -1794,3 +1794,40 @@ module Cryptography {

import ConceptsShared::Cryptography
}

/**
* 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() }

/** Holds if this prompt supplies system-level instructions or assistant content. */
predicate isSystemPrompt() { super.isSystemPrompt() }

/** Holds if this prompt supplies user-level content. */
predicate isUserPrompt() { super.isUserPrompt() }
}

/** 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();

/** Holds if this prompt supplies system-level instructions or assistant content. */
predicate isSystemPrompt() { none() }

/** Holds if this prompt supplies user-level content. */
predicate isUserPrompt() { none() }
}
}
4 changes: 4 additions & 0 deletions python/ql/lib/semmle/python/Frameworks.qll
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ private import semmle.python.frameworks.Aiomysql
private import semmle.python.frameworks.Aiopg
private import semmle.python.frameworks.Aiosqlite
private import semmle.python.frameworks.Airspeed
private import semmle.python.frameworks.Anthropic
private import semmle.python.frameworks.Anyio
private import semmle.python.frameworks.Asyncpg
private import semmle.python.frameworks.Baize
Expand All @@ -34,6 +35,7 @@ private import semmle.python.frameworks.Flask
private import semmle.python.frameworks.FlaskAdmin
private import semmle.python.frameworks.FlaskSqlAlchemy
private import semmle.python.frameworks.Genshi
private import semmle.python.frameworks.GoogleGenAI
private import semmle.python.frameworks.Gradio
private import semmle.python.frameworks.Hdbcli
private import semmle.python.frameworks.Httpx
Expand All @@ -54,6 +56,8 @@ 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.OpenRouter
private import semmle.python.frameworks.Opml
private import semmle.python.frameworks.Oracledb
private import semmle.python.frameworks.Pandas
Expand Down
69 changes: 69 additions & 0 deletions python/ql/lib/semmle/python/frameworks/Anthropic.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Provides classes modeling security-relevant aspects of the `anthropic` package.
* See https://github.com/anthropics/anthropic-sdk-python.
*
* Structurally typed sinks (the `system` field) are modeled via Models as Data:
* python/ql/lib/semmle/python/frameworks/anthropic.model.yml
*
* This file retains only role-filtered message sinks that require inspecting a
* sibling `role` key, which MaD cannot express.
*/

private import python
private import semmle.python.ApiGraphs
private import semmle.python.Concepts

/** Provides classes modeling prompt-injection sinks of the `anthropic` package. */
module Anthropic {
/** Gets a reference to an `anthropic.Anthropic` client instance. */
private API::Node classRef() {
result = API::moduleImport("anthropic").getMember(["Anthropic", "AsyncAnthropic"]).getReturn()
}

/** Gets the message dictionaries passed to `messages.create`/`messages.stream` (stable and beta). */
private API::Node messageElement() {
exists(API::Node create |
create = classRef().getMember("messages").getMember(["create", "stream"])
or
create = classRef().getMember("beta").getMember("messages").getMember(["create", "stream"])
|
result = create.getKeywordParameter("messages").getASubscript()
)
}

/**
* Gets role-filtered system/assistant message content sinks that MaD cannot express.
*/
API::Node getSystemOrAssistantPromptNode() {
exists(API::Node msg |
msg = messageElement() and
msg.getSubscript("role").getAValueReachingSink().asExpr().(StringLiteral).getText() =
["system", "assistant"]
|
result = msg.getSubscript("content")
)
}

/**
* Gets role-filtered user message content sinks that MaD cannot express.
*/
API::Node getUserPromptNode() {
exists(API::Node msg |
msg = messageElement() and
not msg.getSubscript("role").getAValueReachingSink().asExpr().(StringLiteral).getText() =
["system", "assistant"]
|
result = msg.getSubscript("content")
)
}

private class MessagePrompt extends AIPrompt::Range {
MessagePrompt() { this = [getSystemOrAssistantPromptNode(), getUserPromptNode()].asSink() }

override DataFlow::Node getAPrompt() { result = this }

override predicate isSystemPrompt() { this = getSystemOrAssistantPromptNode().asSink() }

override predicate isUserPrompt() { this = getUserPromptNode().asSink() }
}
}
69 changes: 69 additions & 0 deletions python/ql/lib/semmle/python/frameworks/GoogleGenAI.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Provides classes modeling security-relevant aspects of the `google-genai` package.
* See https://github.com/googleapis/python-genai.
*
* Structurally typed sinks (`system_instruction`, `contents`, etc.) are modeled via
* Models as Data: python/ql/lib/semmle/python/frameworks/google-genai.model.yml
*
* This file retains only role-filtered content sinks that require inspecting a
* sibling `role` key, which MaD cannot express.
*/

private import python
private import semmle.python.ApiGraphs
private import semmle.python.Concepts

/** Provides classes modeling prompt-injection sinks of the `google-genai` package. */
module GoogleGenAI {
/** Gets a reference to a `google.genai.Client` instance. */
private API::Node clientRef() {
result = API::moduleImport("google").getMember("genai").getMember("Client").getReturn()
}

/** Gets the content dictionaries passed to `models.generate_content`/`generate_content_stream`. */
private API::Node contentElement() {
result =
clientRef()
.getMember("models")
.getMember(["generate_content", "generate_content_stream"])
.getKeywordParameter("contents")
.getASubscript()
}

/**
* Gets role-filtered system/model content sinks that MaD cannot express.
* Gemini uses the "model" role instead of "assistant".
*/
API::Node getSystemOrAssistantPromptNode() {
exists(API::Node msg |
msg = contentElement() and
msg.getSubscript("role").getAValueReachingSink().asExpr().(StringLiteral).getText() =
["system", "model"]
|
result = msg.getSubscript("parts").getASubscript().getSubscript("text")
)
}

/**
* Gets role-filtered user content sinks that MaD cannot express.
*/
API::Node getUserPromptNode() {
exists(API::Node msg |
msg = contentElement() and
not msg.getSubscript("role").getAValueReachingSink().asExpr().(StringLiteral).getText() =
["system", "model"]
|
result = msg.getSubscript("parts").getASubscript().getSubscript("text")
)
}

private class ContentPrompt extends AIPrompt::Range {
ContentPrompt() { this = [getSystemOrAssistantPromptNode(), getUserPromptNode()].asSink() }

override DataFlow::Node getAPrompt() { result = this }

override predicate isSystemPrompt() { this = getSystemOrAssistantPromptNode().asSink() }

override predicate isUserPrompt() { this = getUserPromptNode().asSink() }
}
}
Loading
Loading