mirror of
https://github.com/github/codeql.git
synced 2026-06-10 23:41:09 +02:00
add openrouter support
This commit is contained in:
19
javascript/ql/lib/ext/openrouter.model.yml
Normal file
19
javascript/ql/lib/ext/openrouter.model.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
extensions:
|
||||
- addsTo:
|
||||
pack: codeql/javascript-all
|
||||
extensible: typeModel
|
||||
data:
|
||||
- ["openrouter.Client", "@openrouter/sdk", "Instance"]
|
||||
- ["openrouter.Client", "@openrouter/sdk", "Member[OpenRouter].Instance"]
|
||||
- ["openrouter.Agent", "@openrouter/agent", "Member[OpenRouter].Instance"]
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/javascript-all
|
||||
extensible: sinkModel
|
||||
data:
|
||||
- ["@openrouter/agent", "Member[callModel].Argument[0].Member[instructions]", "system-prompt-injection"]
|
||||
- ["openrouter.Agent", "Member[callModel].Argument[0].Member[instructions]", "system-prompt-injection"]
|
||||
- ["@openrouter/agent", "Member[tool].Argument[0].Member[description]", "system-prompt-injection"]
|
||||
- ["openrouter.Client", "Member[embeddings].Member[create].Argument[0].Member[input]", "user-prompt-injection"]
|
||||
- ["@openrouter/agent", "Member[callModel].Argument[0].Member[input]", "user-prompt-injection"]
|
||||
- ["openrouter.Agent", "Member[callModel].Argument[0].Member[input]", "user-prompt-injection"]
|
||||
124
javascript/ql/lib/semmle/javascript/frameworks/OpenRouter.qll
Normal file
124
javascript/ql/lib/semmle/javascript/frameworks/OpenRouter.qll
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the OpenRouter JS/TS SDKs.
|
||||
* See https://openrouter.ai/docs/client-sdks/typescript (`@openrouter/sdk`) and
|
||||
* https://openrouter.ai/docs/agent-sdk/overview (`@openrouter/agent`).
|
||||
*
|
||||
* Structurally typed sinks (instructions, input, description, etc.) have been moved to
|
||||
* Models as Data: javascript/ql/lib/ext/openrouter.model.yml
|
||||
*
|
||||
* This file retains only role-filtered sinks that require inspecting a sibling
|
||||
* `role` property, which MaD cannot express.
|
||||
*/
|
||||
|
||||
private import javascript
|
||||
|
||||
/** Holds if `msg` is a message array element with a privileged role. */
|
||||
private predicate isSystemOrDevMessage(API::Node msg) {
|
||||
msg.getMember("role").asSink().mayHaveStringValue(["system", "developer", "assistant"])
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the OpenRouter Client SDK (`@openrouter/sdk`).
|
||||
*/
|
||||
module OpenRouter {
|
||||
/** Gets a reference to an `@openrouter/sdk` client instance. */
|
||||
private API::Node clientRef() {
|
||||
// Default export: import OpenRouter from '@openrouter/sdk'; new OpenRouter()
|
||||
result = API::moduleImport("@openrouter/sdk").getInstance()
|
||||
or
|
||||
// Named import: import { OpenRouter } from '@openrouter/sdk'; new OpenRouter()
|
||||
result = API::moduleImport("@openrouter/sdk").getMember("OpenRouter").getInstance()
|
||||
}
|
||||
|
||||
/** Gets the parameter object of a chat completion call. */
|
||||
private API::Node chatCreateParams() {
|
||||
// client.chat.send({ messages: [...] })
|
||||
result = clientRef().getMember("chat").getMember("send").getParameter(0)
|
||||
or
|
||||
// OpenAI-compatible surface: client.chat.completions.create({ messages: [...] })
|
||||
result =
|
||||
clientRef().getMember("chat").getMember("completions").getMember("create").getParameter(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets role-filtered system/developer/assistant message sinks.
|
||||
* These require checking a sibling `role` property and cannot be expressed in MaD.
|
||||
*/
|
||||
API::Node getSystemOrAssistantPromptNode() {
|
||||
// chat.send/completions.create({ messages: [{ role: "system"/"developer"/"assistant", content: ... }] })
|
||||
exists(API::Node msg, API::Node content |
|
||||
msg = chatCreateParams().getMember("messages").getArrayElement() and
|
||||
isSystemOrDevMessage(msg) and
|
||||
content = msg.getMember("content")
|
||||
|
|
||||
result = content
|
||||
or
|
||||
result = content.getArrayElement().getMember("text")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets role-filtered user message sinks.
|
||||
* These require checking a sibling `role` property and cannot be expressed in MaD.
|
||||
*/
|
||||
API::Node getUserPromptNode() {
|
||||
// chat.send/completions.create({ messages: [{ role: "user", content: ... }] })
|
||||
exists(API::Node msg, API::Node content |
|
||||
msg = chatCreateParams().getMember("messages").getArrayElement() and
|
||||
not isSystemOrDevMessage(msg) and
|
||||
content = msg.getMember("content")
|
||||
|
|
||||
result = content
|
||||
or
|
||||
result = content.getArrayElement().getMember("text")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the OpenRouter Agent SDK (`@openrouter/agent`).
|
||||
*
|
||||
* Structurally typed sinks have been moved to openrouter.model.yml.
|
||||
* This module retains only role-filtered sinks that MaD cannot express.
|
||||
*/
|
||||
module OpenRouterAgent {
|
||||
/** Gets a reference to the `@openrouter/agent` module. */
|
||||
private API::Node moduleRef() { result = API::moduleImport("@openrouter/agent") }
|
||||
|
||||
/** Gets a `callModel` invocation's parameter object (top-level and instance forms). */
|
||||
private API::Node callModelParams() {
|
||||
// import { callModel } from '@openrouter/agent'; callModel({ ... })
|
||||
result = moduleRef().getMember("callModel").getParameter(0)
|
||||
or
|
||||
// import { OpenRouter } from '@openrouter/agent'; new OpenRouter(...).callModel({ ... })
|
||||
result = moduleRef().getMember("OpenRouter").getInstance().getMember("callModel").getParameter(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets role-filtered system/developer/assistant message sinks.
|
||||
* These require checking a sibling `role` property and cannot be expressed in MaD.
|
||||
*/
|
||||
API::Node getSystemOrAssistantPromptNode() {
|
||||
// callModel({ messages/input: [{ role: "system"/"developer"/"assistant", content: ... }] })
|
||||
exists(API::Node msg |
|
||||
msg = callModelParams().getMember(["messages", "input"]).getArrayElement() and
|
||||
isSystemOrDevMessage(msg)
|
||||
|
|
||||
result = msg.getMember("content")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets role-filtered user message sinks.
|
||||
* These require checking a sibling `role` property and cannot be expressed in MaD.
|
||||
*/
|
||||
API::Node getUserPromptNode() {
|
||||
// callModel({ messages/input: [{ role: "user", content: ... }] })
|
||||
exists(API::Node msg |
|
||||
msg = callModelParams().getMember(["messages", "input"]).getArrayElement() and
|
||||
not isSystemOrDevMessage(msg)
|
||||
|
|
||||
result = msg.getMember("content")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ private import semmle.javascript.frameworks.data.ModelsAsData
|
||||
private import semmle.javascript.frameworks.OpenAI
|
||||
private import semmle.javascript.frameworks.Anthropic
|
||||
private import semmle.javascript.frameworks.GoogleGenAI
|
||||
private import semmle.javascript.frameworks.OpenRouter
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -64,6 +65,10 @@ module SystemPromptInjection {
|
||||
this = Anthropic::getSystemOrAssistantPromptNode().asSink()
|
||||
or
|
||||
this = GoogleGenAI::getSystemOrAssistantPromptNode().asSink()
|
||||
or
|
||||
this = OpenRouter::getSystemOrAssistantPromptNode().asSink()
|
||||
or
|
||||
this = OpenRouterAgent::getSystemOrAssistantPromptNode().asSink()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ private import semmle.javascript.frameworks.data.ModelsAsData
|
||||
private import semmle.javascript.frameworks.OpenAI
|
||||
private import semmle.javascript.frameworks.Anthropic
|
||||
private import semmle.javascript.frameworks.GoogleGenAI
|
||||
private import semmle.javascript.frameworks.OpenRouter
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -65,6 +66,10 @@ module UserPromptInjection {
|
||||
this = GoogleGenAI::getUserPromptNode().asSink()
|
||||
or
|
||||
this = AgentSDK::getUserPromptNode().asSink()
|
||||
or
|
||||
this = OpenRouter::getUserPromptNode().asSink()
|
||||
or
|
||||
this = OpenRouterAgent::getUserPromptNode().asSink()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,25 @@ edges
|
||||
| openai_test.js:158:52:158:58 | persona | openai_test.js:158:30:158:58 | "Also t ... persona | provenance | |
|
||||
| openai_test.js:164:31:164:37 | persona | openai_test.js:164:14:164:37 | "Talk l ... persona | provenance | |
|
||||
| openai_test.js:192:49:192:55 | persona | openai_test.js:192:32:192:55 | "Talk l ... persona | provenance | |
|
||||
| openrouter_test.js:12:9:12:15 | persona | openrouter_test.js:23:35:23:41 | persona | provenance | |
|
||||
| openrouter_test.js:12:9:12:15 | persona | openrouter_test.js:38:35:38:41 | persona | provenance | |
|
||||
| openrouter_test.js:12:9:12:15 | persona | openrouter_test.js:52:36:52:42 | persona | provenance | |
|
||||
| openrouter_test.js:12:9:12:15 | persona | openrouter_test.js:78:35:78:41 | persona | provenance | |
|
||||
| openrouter_test.js:12:9:12:15 | persona | openrouter_test.js:88:36:88:42 | persona | provenance | |
|
||||
| openrouter_test.js:12:9:12:15 | persona | openrouter_test.js:98:35:98:41 | persona | provenance | |
|
||||
| openrouter_test.js:12:9:12:15 | persona | openrouter_test.js:109:35:109:41 | persona | provenance | |
|
||||
| openrouter_test.js:12:9:12:15 | persona | openrouter_test.js:118:36:118:42 | persona | provenance | |
|
||||
| openrouter_test.js:12:9:12:15 | persona | openrouter_test.js:125:35:125:41 | persona | provenance | |
|
||||
| openrouter_test.js:12:19:12:35 | req.query.persona | openrouter_test.js:12:9:12:15 | persona | provenance | |
|
||||
| openrouter_test.js:23:35:23:41 | persona | openrouter_test.js:23:18:23:41 | "Talk l ... persona | provenance | |
|
||||
| openrouter_test.js:38:35:38:41 | persona | openrouter_test.js:38:18:38:41 | "Talk l ... persona | provenance | |
|
||||
| openrouter_test.js:52:36:52:42 | persona | openrouter_test.js:52:19:52:42 | "Talk l ... persona | provenance | |
|
||||
| openrouter_test.js:78:35:78:41 | persona | openrouter_test.js:78:18:78:41 | "Talk l ... persona | provenance | |
|
||||
| openrouter_test.js:88:36:88:42 | persona | openrouter_test.js:88:19:88:42 | "Talk l ... persona | provenance | |
|
||||
| openrouter_test.js:98:35:98:41 | persona | openrouter_test.js:98:18:98:41 | "Talk l ... persona | provenance | |
|
||||
| openrouter_test.js:109:35:109:41 | persona | openrouter_test.js:109:18:109:41 | "Talk l ... persona | provenance | |
|
||||
| openrouter_test.js:118:36:118:42 | persona | openrouter_test.js:118:19:118:42 | "Talk l ... persona | provenance | |
|
||||
| openrouter_test.js:125:35:125:41 | persona | openrouter_test.js:125:18:125:41 | "Talk l ... persona | provenance | |
|
||||
nodes
|
||||
| agents_test.js:8:9:8:15 | persona | semmle.label | persona |
|
||||
| agents_test.js:8:19:8:35 | req.query.persona | semmle.label | req.query.persona |
|
||||
@@ -195,6 +214,26 @@ nodes
|
||||
| openai_test.js:164:31:164:37 | persona | semmle.label | persona |
|
||||
| openai_test.js:192:32:192:55 | "Talk l ... persona | semmle.label | "Talk l ... persona |
|
||||
| openai_test.js:192:49:192:55 | persona | semmle.label | persona |
|
||||
| openrouter_test.js:12:9:12:15 | persona | semmle.label | persona |
|
||||
| openrouter_test.js:12:19:12:35 | req.query.persona | semmle.label | req.query.persona |
|
||||
| openrouter_test.js:23:18:23:41 | "Talk l ... persona | semmle.label | "Talk l ... persona |
|
||||
| openrouter_test.js:23:35:23:41 | persona | semmle.label | persona |
|
||||
| openrouter_test.js:38:18:38:41 | "Talk l ... persona | semmle.label | "Talk l ... persona |
|
||||
| openrouter_test.js:38:35:38:41 | persona | semmle.label | persona |
|
||||
| openrouter_test.js:52:19:52:42 | "Talk l ... persona | semmle.label | "Talk l ... persona |
|
||||
| openrouter_test.js:52:36:52:42 | persona | semmle.label | persona |
|
||||
| openrouter_test.js:78:18:78:41 | "Talk l ... persona | semmle.label | "Talk l ... persona |
|
||||
| openrouter_test.js:78:35:78:41 | persona | semmle.label | persona |
|
||||
| openrouter_test.js:88:19:88:42 | "Talk l ... persona | semmle.label | "Talk l ... persona |
|
||||
| openrouter_test.js:88:36:88:42 | persona | semmle.label | persona |
|
||||
| openrouter_test.js:98:18:98:41 | "Talk l ... persona | semmle.label | "Talk l ... persona |
|
||||
| openrouter_test.js:98:35:98:41 | persona | semmle.label | persona |
|
||||
| openrouter_test.js:109:18:109:41 | "Talk l ... persona | semmle.label | "Talk l ... persona |
|
||||
| openrouter_test.js:109:35:109:41 | persona | semmle.label | persona |
|
||||
| openrouter_test.js:118:19:118:42 | "Talk l ... persona | semmle.label | "Talk l ... persona |
|
||||
| openrouter_test.js:118:36:118:42 | persona | semmle.label | persona |
|
||||
| openrouter_test.js:125:18:125:41 | "Talk l ... persona | semmle.label | "Talk l ... persona |
|
||||
| openrouter_test.js:125:35:125:41 | persona | semmle.label | persona |
|
||||
subpaths
|
||||
#select
|
||||
| agents_test.js:16:19:16:42 | "Talk l ... persona | agents_test.js:8:19:8:35 | req.query.persona | agents_test.js:16:19:16:42 | "Talk l ... persona | This prompt construction depends on a $@. | agents_test.js:8:19:8:35 | req.query.persona | user-provided value |
|
||||
@@ -236,3 +275,12 @@ subpaths
|
||||
| openai_test.js:158:30:158:58 | "Also t ... persona | openai_test.js:11:19:11:35 | req.query.persona | openai_test.js:158:30:158:58 | "Also t ... persona | This prompt construction depends on a $@. | openai_test.js:11:19:11:35 | req.query.persona | user-provided value |
|
||||
| openai_test.js:164:14:164:37 | "Talk l ... persona | openai_test.js:11:19:11:35 | req.query.persona | openai_test.js:164:14:164:37 | "Talk l ... persona | This prompt construction depends on a $@. | openai_test.js:11:19:11:35 | req.query.persona | user-provided value |
|
||||
| openai_test.js:192:32:192:55 | "Talk l ... persona | openai_test.js:11:19:11:35 | req.query.persona | openai_test.js:192:32:192:55 | "Talk l ... persona | This prompt construction depends on a $@. | openai_test.js:11:19:11:35 | req.query.persona | user-provided value |
|
||||
| openrouter_test.js:23:18:23:41 | "Talk l ... persona | openrouter_test.js:12:19:12:35 | req.query.persona | openrouter_test.js:23:18:23:41 | "Talk l ... persona | This prompt construction depends on a $@. | openrouter_test.js:12:19:12:35 | req.query.persona | user-provided value |
|
||||
| openrouter_test.js:38:18:38:41 | "Talk l ... persona | openrouter_test.js:12:19:12:35 | req.query.persona | openrouter_test.js:38:18:38:41 | "Talk l ... persona | This prompt construction depends on a $@. | openrouter_test.js:12:19:12:35 | req.query.persona | user-provided value |
|
||||
| openrouter_test.js:52:19:52:42 | "Talk l ... persona | openrouter_test.js:12:19:12:35 | req.query.persona | openrouter_test.js:52:19:52:42 | "Talk l ... persona | This prompt construction depends on a $@. | openrouter_test.js:12:19:12:35 | req.query.persona | user-provided value |
|
||||
| openrouter_test.js:78:18:78:41 | "Talk l ... persona | openrouter_test.js:12:19:12:35 | req.query.persona | openrouter_test.js:78:18:78:41 | "Talk l ... persona | This prompt construction depends on a $@. | openrouter_test.js:12:19:12:35 | req.query.persona | user-provided value |
|
||||
| openrouter_test.js:88:19:88:42 | "Talk l ... persona | openrouter_test.js:12:19:12:35 | req.query.persona | openrouter_test.js:88:19:88:42 | "Talk l ... persona | This prompt construction depends on a $@. | openrouter_test.js:12:19:12:35 | req.query.persona | user-provided value |
|
||||
| openrouter_test.js:98:18:98:41 | "Talk l ... persona | openrouter_test.js:12:19:12:35 | req.query.persona | openrouter_test.js:98:18:98:41 | "Talk l ... persona | This prompt construction depends on a $@. | openrouter_test.js:12:19:12:35 | req.query.persona | user-provided value |
|
||||
| openrouter_test.js:109:18:109:41 | "Talk l ... persona | openrouter_test.js:12:19:12:35 | req.query.persona | openrouter_test.js:109:18:109:41 | "Talk l ... persona | This prompt construction depends on a $@. | openrouter_test.js:12:19:12:35 | req.query.persona | user-provided value |
|
||||
| openrouter_test.js:118:19:118:42 | "Talk l ... persona | openrouter_test.js:12:19:12:35 | req.query.persona | openrouter_test.js:118:19:118:42 | "Talk l ... persona | This prompt construction depends on a $@. | openrouter_test.js:12:19:12:35 | req.query.persona | user-provided value |
|
||||
| openrouter_test.js:125:18:125:41 | "Talk l ... persona | openrouter_test.js:12:19:12:35 | req.query.persona | openrouter_test.js:125:18:125:41 | "Talk l ... persona | This prompt construction depends on a $@. | openrouter_test.js:12:19:12:35 | req.query.persona | user-provided value |
|
||||
|
||||
@@ -13,7 +13,7 @@ app.get("/agents", async (req, res) => {
|
||||
// SHOULD ALERT
|
||||
const agent1 = new Agent({
|
||||
name: "Assistant",
|
||||
instructions: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
instructions: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// === Agent constructor: instructions as lambda ===
|
||||
@@ -22,7 +22,7 @@ app.get("/agents", async (req, res) => {
|
||||
const agent2 = new Agent({
|
||||
name: "Dynamic",
|
||||
instructions: (runContext) => {
|
||||
return "Talk like a " + persona; // $ Alert[js/prompt-injection]
|
||||
return "Talk like a " + persona; // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ app.get("/agents", async (req, res) => {
|
||||
const agent3 = new Agent({
|
||||
name: "AsyncDynamic",
|
||||
instructions: async (runContext) => {
|
||||
return "Talk like a " + persona; // $ Alert[js/prompt-injection]
|
||||
return "Talk like a " + persona; // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ app.get("/agents", async (req, res) => {
|
||||
const agent4 = new Agent({
|
||||
name: "Specialist",
|
||||
instructions: "Help with refunds",
|
||||
handoffDescription: "Handles " + persona, // $ Alert[js/prompt-injection]
|
||||
handoffDescription: "Handles " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// === agent.asTool(): toolDescription ===
|
||||
@@ -48,7 +48,7 @@ app.get("/agents", async (req, res) => {
|
||||
// SHOULD ALERT
|
||||
agent1.asTool({
|
||||
toolName: "helper",
|
||||
toolDescription: "Ask about " + persona, // $ Alert[js/prompt-injection]
|
||||
toolDescription: "Ask about " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// === tool(): description ===
|
||||
@@ -56,7 +56,7 @@ app.get("/agents", async (req, res) => {
|
||||
// SHOULD ALERT
|
||||
const myTool = tool({
|
||||
name: "lookup",
|
||||
description: "Look up info about " + persona, // $ Alert[js/prompt-injection]
|
||||
description: "Look up info about " + persona, // $ Alert[js/system-prompt-injection]
|
||||
parameters: z.object({ query: z.string() }),
|
||||
execute: async ({ query }) => "result",
|
||||
});
|
||||
@@ -70,7 +70,7 @@ app.get("/agents", async (req, res) => {
|
||||
|
||||
// SHOULD ALERT
|
||||
const r2 = await run(agent1, [
|
||||
{ role: "system", content: "Talk like a " + persona }, // $ Alert[js/prompt-injection]
|
||||
{ role: "system", content: "Talk like a " + persona }, // $ Alert[js/system-prompt-injection]
|
||||
{ role: "user", content: query },
|
||||
]);
|
||||
|
||||
@@ -78,7 +78,7 @@ app.get("/agents", async (req, res) => {
|
||||
|
||||
// SHOULD ALERT
|
||||
const r3 = await run(agent1, [
|
||||
{ role: "developer", content: "Talk like a " + persona }, // $ Alert[js/prompt-injection]
|
||||
{ role: "developer", content: "Talk like a " + persona }, // $ Alert[js/system-prompt-injection]
|
||||
]);
|
||||
|
||||
// === run() with array input: user role ===
|
||||
@@ -93,7 +93,7 @@ app.get("/agents", async (req, res) => {
|
||||
// SHOULD ALERT
|
||||
const runner = new Runner();
|
||||
const r5 = await runner.run(agent1, [
|
||||
{ role: "system", content: "Talk like a " + persona }, // $ Alert[js/prompt-injection]
|
||||
{ role: "system", content: "Talk like a " + persona }, // $ Alert[js/system-prompt-injection]
|
||||
]);
|
||||
|
||||
// === Sanitizer: constant comparison ===
|
||||
|
||||
@@ -14,7 +14,7 @@ app.get("/test", async (req, res) => {
|
||||
const m1 = await client.messages.create({
|
||||
model: "claude-sonnet-4-20250514",
|
||||
max_tokens: 1024,
|
||||
system: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
system: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
messages: [{ role: "user", content: query }],
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ app.get("/test", async (req, res) => {
|
||||
system: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
text: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
],
|
||||
messages: [{ role: "user", content: query }],
|
||||
@@ -42,7 +42,7 @@ app.get("/test", async (req, res) => {
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
content: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
{ role: "user", content: query },
|
||||
],
|
||||
@@ -68,7 +68,7 @@ app.get("/test", async (req, res) => {
|
||||
const bm1 = await client.beta.messages.create({
|
||||
model: "claude-sonnet-4-20250514",
|
||||
max_tokens: 1024,
|
||||
system: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
system: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
messages: [{ role: "user", content: query }],
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ app.get("/test", async (req, res) => {
|
||||
system: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
text: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
],
|
||||
messages: [{ role: "user", content: query }],
|
||||
@@ -96,7 +96,7 @@ app.get("/test", async (req, res) => {
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
content: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
{ role: "user", content: query },
|
||||
],
|
||||
@@ -107,14 +107,14 @@ app.get("/test", async (req, res) => {
|
||||
// SHOULD ALERT
|
||||
const ba1 = await client.beta.agents.create({
|
||||
model: "claude-sonnet-4-20250514",
|
||||
system: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
system: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// === beta.agents.update: system ===
|
||||
|
||||
// SHOULD ALERT
|
||||
await client.beta.agents.update("agent_123", {
|
||||
system: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
system: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// === Barrier: user-role content in shared message array ===
|
||||
@@ -138,7 +138,7 @@ app.get("/test", async (req, res) => {
|
||||
// SHOULD ALERT — tainted data goes into system role; barrier on user role
|
||||
// must not suppress the system-role taint path.
|
||||
const messages2 = [
|
||||
{ role: "system", content: "Talk like a " + persona }, // $ Alert[js/prompt-injection]
|
||||
{ role: "system", content: "Talk like a " + persona }, // $ Alert[js/system-prompt-injection]
|
||||
{ role: "user", content: query },
|
||||
];
|
||||
const systemMsg2 = messages2.find((m) => m.role === "system");
|
||||
|
||||
@@ -15,7 +15,7 @@ app.get("/test", async (req, res) => {
|
||||
model: "gemini-2.0-flash",
|
||||
contents: "Hello",
|
||||
config: {
|
||||
systemInstruction: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
systemInstruction: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ app.get("/test", async (req, res) => {
|
||||
contents: [
|
||||
{
|
||||
role: "model",
|
||||
parts: [{ text: "Talk like a " + persona }], // $ Alert[js/prompt-injection]
|
||||
parts: [{ text: "Talk like a " + persona }], // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
@@ -56,7 +56,7 @@ app.get("/test", async (req, res) => {
|
||||
model: "gemini-2.0-flash",
|
||||
contents: "Hello",
|
||||
config: {
|
||||
systemInstruction: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
systemInstruction: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@ app.get("/test", async (req, res) => {
|
||||
// SHOULD ALERT
|
||||
const g5 = await ai.models.generateImages({
|
||||
model: "imagen-3.0-generate-002",
|
||||
prompt: "Draw a picture of " + persona, // $ Alert[js/prompt-injection]
|
||||
prompt: "Draw a picture of " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// === editImage: prompt ===
|
||||
@@ -73,7 +73,7 @@ app.get("/test", async (req, res) => {
|
||||
// SHOULD ALERT
|
||||
const g6 = await ai.models.editImage({
|
||||
model: "imagen-3.0-capability-001",
|
||||
prompt: "Edit to look like " + persona, // $ Alert[js/prompt-injection]
|
||||
prompt: "Edit to look like " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// === chats.create: systemInstruction ===
|
||||
@@ -82,7 +82,7 @@ app.get("/test", async (req, res) => {
|
||||
const chat = ai.chats.create({
|
||||
model: "gemini-2.0-flash",
|
||||
config: {
|
||||
systemInstruction: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
systemInstruction: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
});
|
||||
|
||||
@@ -92,7 +92,7 @@ app.get("/test", async (req, res) => {
|
||||
await chat.sendMessage({
|
||||
message: query,
|
||||
config: {
|
||||
systemInstruction: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
systemInstruction: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@ app.get("/test", async (req, res) => {
|
||||
const session = await ai.live.connect({
|
||||
model: "gemini-2.0-flash-live-001",
|
||||
config: {
|
||||
systemInstruction: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
systemInstruction: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
callbacks: {
|
||||
onmessage: (msg) => {},
|
||||
|
||||
@@ -13,16 +13,16 @@ app.get("/test", async (req, res) => {
|
||||
|
||||
// === SystemMessage (SHOULD ALERT) ===
|
||||
|
||||
const sysMsg1 = new SystemMessage("Talk like a " + persona); // $ Alert[js/prompt-injection]
|
||||
const sysMsg1 = new SystemMessage("Talk like a " + persona); // $ Alert[js/system-prompt-injection]
|
||||
|
||||
const sysMsg2 = new SystemMessage({
|
||||
content: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
content: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// === createAgent with systemPrompt (SHOULD ALERT) ===
|
||||
|
||||
const agent = createAgent({
|
||||
systemPrompt: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
systemPrompt: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// === Barrier test: user role content in shared array (SHOULD NOT ALERT) ===
|
||||
|
||||
@@ -16,7 +16,7 @@ app.get("/test", async (req, res) => {
|
||||
// instructions: tainted string (SHOULD ALERT)
|
||||
const r1 = await client.responses.create({
|
||||
model: "gpt-4.1",
|
||||
instructions: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
instructions: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
input: "Hello",
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ app.get("/test", async (req, res) => {
|
||||
input: [
|
||||
{
|
||||
role: "system",
|
||||
content: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
content: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
@@ -41,7 +41,7 @@ app.get("/test", async (req, res) => {
|
||||
input: [
|
||||
{
|
||||
role: "developer",
|
||||
content: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
content: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -65,7 +65,7 @@ app.get("/test", async (req, res) => {
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
content: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
@@ -80,7 +80,7 @@ app.get("/test", async (req, res) => {
|
||||
messages: [
|
||||
{
|
||||
role: "developer",
|
||||
content: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
content: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -94,7 +94,7 @@ app.get("/test", async (req, res) => {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
text: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -107,7 +107,7 @@ app.get("/test", async (req, res) => {
|
||||
messages: [
|
||||
{
|
||||
role: "developer",
|
||||
content: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
content: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -117,19 +117,19 @@ app.get("/test", async (req, res) => {
|
||||
// prompt (SHOULD ALERT)
|
||||
const l1 = await client.completions.create({
|
||||
model: "gpt-3.5-turbo-instruct",
|
||||
prompt: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
prompt: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// === Images API ===
|
||||
|
||||
// images.generate (SHOULD ALERT)
|
||||
const i1 = await client.images.generate({
|
||||
prompt: "Draw a picture of " + persona, // $ Alert[js/prompt-injection]
|
||||
prompt: "Draw a picture of " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// images.edit (SHOULD ALERT)
|
||||
const i2 = await client.images.edit({
|
||||
prompt: "Edit to look like " + persona, // $ Alert[js/prompt-injection]
|
||||
prompt: "Edit to look like " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// === Assistants API (beta) ===
|
||||
@@ -138,30 +138,30 @@ app.get("/test", async (req, res) => {
|
||||
const a1 = await client.beta.assistants.create({
|
||||
name: "Test Agent",
|
||||
model: "gpt-4.1",
|
||||
instructions: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
instructions: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// assistants.update (SHOULD ALERT)
|
||||
await client.beta.assistants.update("asst_123", {
|
||||
instructions: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
instructions: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// threads.runs.create (SHOULD ALERT)
|
||||
const tr1 = await client.beta.threads.runs.create("thread_123", {
|
||||
assistant_id: "asst_123",
|
||||
instructions: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
instructions: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// threads.runs.create with additional_instructions (SHOULD ALERT)
|
||||
const tr2 = await client.beta.threads.runs.create("thread_123", {
|
||||
assistant_id: "asst_123",
|
||||
additional_instructions: "Also talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
additional_instructions: "Also talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// threads.messages.create with system role (SHOULD ALERT)
|
||||
await client.beta.threads.messages.create("thread_123", {
|
||||
role: "system",
|
||||
content: "Talk like a " + persona, // $ Alert[js/prompt-injection]
|
||||
content: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// threads.messages.create with user role (SHOULD NOT ALERT)
|
||||
@@ -176,20 +176,20 @@ app.get("/test", async (req, res) => {
|
||||
const at1 = await client.audio.transcriptions.create({
|
||||
file: "audio.mp3",
|
||||
model: "whisper-1",
|
||||
prompt: "Transcribe about " + persona, // $ Alert[js/prompt-injection]
|
||||
prompt: "Transcribe about " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// audio.translations.create (SHOULD ALERT)
|
||||
const atl1 = await client.audio.translations.create({
|
||||
file: "audio.mp3",
|
||||
model: "whisper-1",
|
||||
prompt: "Translate about " + persona, // $ Alert[js/prompt-injection]
|
||||
prompt: "Translate about " + persona, // $ Alert[js/system-prompt-injection]
|
||||
});
|
||||
|
||||
// === Object assigned to variable first ===
|
||||
|
||||
// Should still be caught via data flow
|
||||
const opts = { instructions: "Talk like a " + persona }; // $ Alert[js/prompt-injection]
|
||||
const opts = { instructions: "Talk like a " + persona }; // $ Alert[js/system-prompt-injection]
|
||||
const r5 = await client.responses.create(opts);
|
||||
|
||||
// === Sanitizer: constant comparison ===
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
const express = require("express");
|
||||
const OpenRouter = require("@openrouter/sdk");
|
||||
const { OpenRouter: OpenRouterNamed } = require("@openrouter/sdk");
|
||||
const { callModel, tool } = require("@openrouter/agent");
|
||||
const { OpenRouter: OpenRouterAgent } = require("@openrouter/agent");
|
||||
|
||||
const app = express();
|
||||
const client = new OpenRouter();
|
||||
const namedClient = new OpenRouterNamed();
|
||||
|
||||
app.get("/test", async (req, res) => {
|
||||
const persona = req.query.persona;
|
||||
const query = req.query.query;
|
||||
|
||||
// === OpenRouter Client SDK: chat.send ===
|
||||
|
||||
// messages with system role (SHOULD ALERT)
|
||||
const s1 = await client.chat.send({
|
||||
model: "openai/gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: query, // OK - user role
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// messages with developer role (SHOULD ALERT)
|
||||
const s2 = await client.chat.send({
|
||||
model: "openai/gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "developer",
|
||||
content: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// messages with content as array of content parts (SHOULD ALERT)
|
||||
const s3 = await client.chat.send({
|
||||
model: "openai/gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// messages with user role (SHOULD NOT ALERT)
|
||||
const s4 = await client.chat.send({
|
||||
model: "openai/gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: query, // OK - user role is expected to carry user input
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// === OpenRouter Client SDK: chat.completions.create (OpenAI-compatible) ===
|
||||
|
||||
// messages with system role (SHOULD ALERT)
|
||||
const c1 = await namedClient.chat.completions.create({
|
||||
model: "openai/gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// === OpenRouter Agent SDK: callModel ===
|
||||
|
||||
// instructions: tainted string (SHOULD ALERT)
|
||||
const a1 = await callModel({
|
||||
model: "openai/gpt-4o",
|
||||
instructions: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
input: "Hello",
|
||||
});
|
||||
|
||||
// messages with system role (SHOULD ALERT)
|
||||
const a2 = await callModel({
|
||||
model: "openai/gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// input array with developer role (SHOULD ALERT)
|
||||
const a3 = await callModel({
|
||||
model: "openai/gpt-4o",
|
||||
input: [
|
||||
{
|
||||
role: "developer",
|
||||
content: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// instance form: new OpenRouter().callModel (SHOULD ALERT)
|
||||
const agent = new OpenRouterAgent();
|
||||
const a4 = await agent.callModel({
|
||||
model: "openai/gpt-4o",
|
||||
instructions: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
input: "Hello",
|
||||
});
|
||||
|
||||
// tool description (SHOULD ALERT)
|
||||
const t1 = tool({
|
||||
name: "lookup",
|
||||
description: "Talk like a " + persona, // $ Alert[js/system-prompt-injection]
|
||||
inputSchema: {},
|
||||
execute: async () => {},
|
||||
});
|
||||
|
||||
// input array with user role (SHOULD NOT ALERT)
|
||||
const a5 = await callModel({
|
||||
model: "openai/gpt-4o",
|
||||
input: [
|
||||
{
|
||||
role: "user",
|
||||
content: query, // OK - user role
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
res.send("ok");
|
||||
});
|
||||
@@ -44,6 +44,15 @@ edges
|
||||
| openai_user_test.js:15:9:15:17 | userInput | openai_user_test.js:201:27:201:35 | userInput | provenance | |
|
||||
| openai_user_test.js:15:9:15:17 | userInput | openai_user_test.js:205:30:205:38 | userInput | provenance | |
|
||||
| openai_user_test.js:15:21:15:39 | req.query.userInput | openai_user_test.js:15:9:15:17 | userInput | provenance | |
|
||||
| openrouter_user_test.js:12:9:12:17 | userInput | openrouter_user_test.js:22:18:22:26 | userInput | provenance | |
|
||||
| openrouter_user_test.js:12:9:12:17 | userInput | openrouter_user_test.js:36:19:36:27 | userInput | provenance | |
|
||||
| openrouter_user_test.js:12:9:12:17 | userInput | openrouter_user_test.js:50:18:50:26 | userInput | provenance | |
|
||||
| openrouter_user_test.js:12:9:12:17 | userInput | openrouter_user_test.js:59:12:59:20 | userInput | provenance | |
|
||||
| openrouter_user_test.js:12:9:12:17 | userInput | openrouter_user_test.js:68:12:68:20 | userInput | provenance | |
|
||||
| openrouter_user_test.js:12:9:12:17 | userInput | openrouter_user_test.js:77:18:77:26 | userInput | provenance | |
|
||||
| openrouter_user_test.js:12:9:12:17 | userInput | openrouter_user_test.js:88:18:88:26 | userInput | provenance | |
|
||||
| openrouter_user_test.js:12:9:12:17 | userInput | openrouter_user_test.js:97:12:97:20 | userInput | provenance | |
|
||||
| openrouter_user_test.js:12:21:12:39 | req.query.userInput | openrouter_user_test.js:12:9:12:17 | userInput | provenance | |
|
||||
nodes
|
||||
| anthropic_user_test.js:8:9:8:17 | userInput | semmle.label | userInput |
|
||||
| anthropic_user_test.js:8:21:8:39 | req.query.userInput | semmle.label | req.query.userInput |
|
||||
@@ -94,6 +103,16 @@ nodes
|
||||
| openai_user_test.js:196:30:196:38 | userInput | semmle.label | userInput |
|
||||
| openai_user_test.js:201:27:201:35 | userInput | semmle.label | userInput |
|
||||
| openai_user_test.js:205:30:205:38 | userInput | semmle.label | userInput |
|
||||
| openrouter_user_test.js:12:9:12:17 | userInput | semmle.label | userInput |
|
||||
| openrouter_user_test.js:12:21:12:39 | req.query.userInput | semmle.label | req.query.userInput |
|
||||
| openrouter_user_test.js:22:18:22:26 | userInput | semmle.label | userInput |
|
||||
| openrouter_user_test.js:36:19:36:27 | userInput | semmle.label | userInput |
|
||||
| openrouter_user_test.js:50:18:50:26 | userInput | semmle.label | userInput |
|
||||
| openrouter_user_test.js:59:12:59:20 | userInput | semmle.label | userInput |
|
||||
| openrouter_user_test.js:68:12:68:20 | userInput | semmle.label | userInput |
|
||||
| openrouter_user_test.js:77:18:77:26 | userInput | semmle.label | userInput |
|
||||
| openrouter_user_test.js:88:18:88:26 | userInput | semmle.label | userInput |
|
||||
| openrouter_user_test.js:97:12:97:20 | userInput | semmle.label | userInput |
|
||||
subpaths
|
||||
#select
|
||||
| anthropic_user_test.js:18:18:18:26 | userInput | anthropic_user_test.js:8:21:8:39 | req.query.userInput | anthropic_user_test.js:18:18:18:26 | userInput | This prompt construction depends on a $@. | anthropic_user_test.js:8:21:8:39 | req.query.userInput | user-provided value |
|
||||
@@ -137,3 +156,11 @@ subpaths
|
||||
| openai_user_test.js:196:30:196:38 | userInput | openai_user_test.js:15:21:15:39 | req.query.userInput | openai_user_test.js:196:30:196:38 | userInput | This prompt construction depends on a $@. | openai_user_test.js:15:21:15:39 | req.query.userInput | user-provided value |
|
||||
| openai_user_test.js:201:27:201:35 | userInput | openai_user_test.js:15:21:15:39 | req.query.userInput | openai_user_test.js:201:27:201:35 | userInput | This prompt construction depends on a $@. | openai_user_test.js:15:21:15:39 | req.query.userInput | user-provided value |
|
||||
| openai_user_test.js:205:30:205:38 | userInput | openai_user_test.js:15:21:15:39 | req.query.userInput | openai_user_test.js:205:30:205:38 | userInput | This prompt construction depends on a $@. | openai_user_test.js:15:21:15:39 | req.query.userInput | user-provided value |
|
||||
| openrouter_user_test.js:22:18:22:26 | userInput | openrouter_user_test.js:12:21:12:39 | req.query.userInput | openrouter_user_test.js:22:18:22:26 | userInput | This prompt construction depends on a $@. | openrouter_user_test.js:12:21:12:39 | req.query.userInput | user-provided value |
|
||||
| openrouter_user_test.js:36:19:36:27 | userInput | openrouter_user_test.js:12:21:12:39 | req.query.userInput | openrouter_user_test.js:36:19:36:27 | userInput | This prompt construction depends on a $@. | openrouter_user_test.js:12:21:12:39 | req.query.userInput | user-provided value |
|
||||
| openrouter_user_test.js:50:18:50:26 | userInput | openrouter_user_test.js:12:21:12:39 | req.query.userInput | openrouter_user_test.js:50:18:50:26 | userInput | This prompt construction depends on a $@. | openrouter_user_test.js:12:21:12:39 | req.query.userInput | user-provided value |
|
||||
| openrouter_user_test.js:59:12:59:20 | userInput | openrouter_user_test.js:12:21:12:39 | req.query.userInput | openrouter_user_test.js:59:12:59:20 | userInput | This prompt construction depends on a $@. | openrouter_user_test.js:12:21:12:39 | req.query.userInput | user-provided value |
|
||||
| openrouter_user_test.js:68:12:68:20 | userInput | openrouter_user_test.js:12:21:12:39 | req.query.userInput | openrouter_user_test.js:68:12:68:20 | userInput | This prompt construction depends on a $@. | openrouter_user_test.js:12:21:12:39 | req.query.userInput | user-provided value |
|
||||
| openrouter_user_test.js:77:18:77:26 | userInput | openrouter_user_test.js:12:21:12:39 | req.query.userInput | openrouter_user_test.js:77:18:77:26 | userInput | This prompt construction depends on a $@. | openrouter_user_test.js:12:21:12:39 | req.query.userInput | user-provided value |
|
||||
| openrouter_user_test.js:88:18:88:26 | userInput | openrouter_user_test.js:12:21:12:39 | req.query.userInput | openrouter_user_test.js:88:18:88:26 | userInput | This prompt construction depends on a $@. | openrouter_user_test.js:12:21:12:39 | req.query.userInput | user-provided value |
|
||||
| openrouter_user_test.js:97:12:97:20 | userInput | openrouter_user_test.js:12:21:12:39 | req.query.userInput | openrouter_user_test.js:97:12:97:20 | userInput | This prompt construction depends on a $@. | openrouter_user_test.js:12:21:12:39 | req.query.userInput | user-provided value |
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
const express = require("express");
|
||||
const OpenRouter = require("@openrouter/sdk");
|
||||
const { OpenRouter: OpenRouterNamed } = require("@openrouter/sdk");
|
||||
const { callModel } = require("@openrouter/agent");
|
||||
const { OpenRouter: OpenRouterAgent } = require("@openrouter/agent");
|
||||
|
||||
const app = express();
|
||||
const client = new OpenRouter();
|
||||
const namedClient = new OpenRouterNamed();
|
||||
|
||||
app.get("/test", async (req, res) => {
|
||||
const userInput = req.query.userInput;
|
||||
|
||||
// === OpenRouter Client SDK: chat.send ===
|
||||
|
||||
// messages with user role (SHOULD ALERT)
|
||||
await client.chat.send({
|
||||
model: "openai/gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: userInput, // $ Alert[js/user-prompt-injection]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// messages with user role, content parts (SHOULD ALERT)
|
||||
await client.chat.send({
|
||||
model: "openai/gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: userInput, // $ Alert[js/user-prompt-injection]
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// === OpenRouter Client SDK: chat.completions.create (OpenAI-compatible) ===
|
||||
|
||||
await namedClient.chat.completions.create({
|
||||
model: "openai/gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: userInput, // $ Alert[js/user-prompt-injection]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// === OpenRouter Client SDK: embeddings ===
|
||||
|
||||
await client.embeddings.create({
|
||||
model: "openai/text-embedding-3-small",
|
||||
input: userInput, // $ Alert[js/user-prompt-injection]
|
||||
});
|
||||
|
||||
// === OpenRouter Agent SDK: callModel ===
|
||||
|
||||
// input as string (SHOULD ALERT)
|
||||
await callModel({
|
||||
model: "openai/gpt-4o",
|
||||
instructions: "You are a helpful assistant",
|
||||
input: userInput, // $ Alert[js/user-prompt-injection]
|
||||
});
|
||||
|
||||
// input array with user role (SHOULD ALERT)
|
||||
await callModel({
|
||||
model: "openai/gpt-4o",
|
||||
input: [
|
||||
{
|
||||
role: "user",
|
||||
content: userInput, // $ Alert[js/user-prompt-injection]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// messages with user role (SHOULD ALERT)
|
||||
await callModel({
|
||||
model: "openai/gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: userInput, // $ Alert[js/user-prompt-injection]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// instance form: new OpenRouter().callModel (SHOULD ALERT)
|
||||
const agent = new OpenRouterAgent();
|
||||
await agent.callModel({
|
||||
model: "openai/gpt-4o",
|
||||
input: userInput, // $ Alert[js/user-prompt-injection]
|
||||
});
|
||||
|
||||
res.send("ok");
|
||||
});
|
||||
@@ -1,106 +0,0 @@
|
||||
# `js/prompt-injection` Detection Report
|
||||
|
||||
**Date:** May 15, 2026
|
||||
**Branch:** `bazookamusic/cwe-1427`
|
||||
**Queries:** `SystemPromptInjection.ql`, `UserPromptInjection.ql`
|
||||
|
||||
## Summary
|
||||
|
||||
Evaluated 11 repositories with `js/prompt-injection` findings. **9 True Positives, 2 False Positives.**
|
||||
|
||||
## Detections
|
||||
|
||||
### 1. Harsh5225/CodeBuddy — **TP**
|
||||
|
||||
**Finding:** System prompt injection
|
||||
**Description:** Direct system prompt injection. User-controlled input flows into the system prompt of an LLM call without sanitization.
|
||||
|
||||
---
|
||||
|
||||
### 2. barnesy/momentum (×6 findings) — **TP**
|
||||
|
||||
**Finding:** System prompt injection (6 paths)
|
||||
**Description:** Multiple system prompt injection paths. User input is concatenated or interpolated into system-level prompts across several endpoints.
|
||||
|
||||
---
|
||||
|
||||
### 3. shane-reaume/TalkToDev (×3 findings) — **TP**
|
||||
|
||||
**Finding:** System prompt injection (3 paths)
|
||||
**Description:** Multiple system prompt injection paths. User-controlled data flows into system prompts for LLM calls.
|
||||
|
||||
---
|
||||
|
||||
### 4. huggingface/responses.js — **TP**
|
||||
|
||||
**Finding:** `responses.ts:271`
|
||||
**Description:** An open API endpoint populates the system prompt directly from request data. There is no authentication guarding the endpoint, meaning any caller can control the system-level instructions sent to the model.
|
||||
|
||||
---
|
||||
|
||||
### 5. FlowiseAI/Flowise — **TP**
|
||||
|
||||
**Finding:** `assistants/index.ts:107`
|
||||
**Description:** User input flows into the OpenAI Assistants API `instructions` field. The `instructions` field is a developer-level system prompt — it defines the assistant's behavior and is not designed for end-user content. Even though Flowise has RBAC, authenticated users can craft `instructions` that affect other users' conversations with the created assistant. Exposing this field to user input is a prompt injection vector regardless of authentication.
|
||||
|
||||
---
|
||||
|
||||
### 6. sjinnovation/CollabAI (×2 findings) — **TP**
|
||||
|
||||
**Finding:** `openai.js` (2 paths)
|
||||
**Description:** The POST route for creating OpenAI assistants does **not** have `authenticateUser` middleware applied. Unauthenticated users can create OpenAI assistants with arbitrary `instructions`, directly controlling the system prompt. The missing auth middleware is visible in the route definition — other routes in the same file do use `authenticateUser`.
|
||||
|
||||
---
|
||||
|
||||
### 7. theodi/chat2db — **TP**
|
||||
|
||||
**Finding:** `openaiClient.js:49`
|
||||
**Description:** No authentication on the `/v1/chat/completions` route. The route accepts a `messages` array from the client, which can include `role: "system"` messages. An unauthenticated caller can fully override the system prompt.
|
||||
|
||||
---
|
||||
|
||||
### 8. torarnehave1/mystmkra.io — **TP**
|
||||
|
||||
**Finding:** `assistants.js:58`
|
||||
**Description:** No authentication on `/assistants/*` routes. An `isAuthenticated` middleware exists in the codebase but is **not applied** to the assistant routes. Unauthenticated users can create or modify assistants with arbitrary instructions, controlling the system prompt.
|
||||
|
||||
---
|
||||
|
||||
### 9. kvadou/franchise-manager — **TP**
|
||||
|
||||
**Finding:** `generation.ts:449`
|
||||
**Description:** User-controlled `moduleContext.title` and `moduleContext.description` (from `request.json()`) are concatenated directly into the system prompt. Even with authentication, this is a prompt injection vector: a user can embed instructions like "Ignore all previous instructions" in the title/description fields, overriding the developer's intended system prompt behavior.
|
||||
|
||||
---
|
||||
|
||||
### 10. armando3069/AI-Inbox — **FP**
|
||||
|
||||
**Finding:** `ai-assistant.service.ts:121`
|
||||
**Description:** The system prompt tone is selected from a hardcoded `TONE_PROMPTS` map. User input selects which tone to use (e.g., "professional", "casual"), but the actual prompt text is developer-controlled. The false positive arose from CodeQL's array taint propagation — user-tainted content in a `{role:"user"}` message caused the entire messages array to appear tainted, including the `{role:"system"}` message with the hardcoded tone. **The `UserRoleMessageContentBarrier` now correctly blocks this.**
|
||||
|
||||
---
|
||||
|
||||
### 11. mckaywrigley/chatbot-ui — **FP**
|
||||
|
||||
**Finding:** `anthropic/route.ts:67`
|
||||
**Description:** Users authenticate via Supabase and provide their own Anthropic API key. The "system prompt" is a personal configuration set by the user for their own chatbot instance. The user is effectively the developer in this context — they are configuring their own model's behavior using their own API key. There is no multi-tenant risk; the system prompt only affects the user who set it.
|
||||
|
||||
---
|
||||
|
||||
## Verdict Summary
|
||||
|
||||
| # | Repository | Finding Location | Verdict | Key Factor |
|
||||
|---|-----------|-----------------|---------|------------|
|
||||
| 1 | Harsh5225/CodeBuddy | system prompt | **TP** | Direct injection |
|
||||
| 2 | barnesy/momentum | ×6 locations | **TP** | Multiple injection paths |
|
||||
| 3 | shane-reaume/TalkToDev | ×3 locations | **TP** | Multiple injection paths |
|
||||
| 4 | huggingface/responses.js | `responses.ts:271` | **TP** | Open API, no auth |
|
||||
| 5 | FlowiseAI/Flowise | `assistants/index.ts:107` | **TP** | `instructions` is developer API, not user API |
|
||||
| 6 | sjinnovation/CollabAI | `openai.js` ×2 | **TP** | Missing `authenticateUser` middleware |
|
||||
| 7 | theodi/chat2db | `openaiClient.js:49` | **TP** | No auth, accepts `role:"system"` |
|
||||
| 8 | torarnehave1/mystmkra.io | `assistants.js:58` | **TP** | Auth exists but not applied to routes |
|
||||
| 9 | kvadou/franchise-manager | `generation.ts:449` | **TP** | User content in system prompt position |
|
||||
| 10 | armando3069/AI-Inbox | `ai-assistant.service.ts:121` | **FP** | Hardcoded prompts, array taint propagation |
|
||||
| 11 | mckaywrigley/chatbot-ui | `anthropic/route.ts:67` | **FP** | User's own API key, self-configured |
|
||||
|
||||
**Precision: 9/11 (81.8%)**
|
||||
Reference in New Issue
Block a user