add openrouter support

This commit is contained in:
BazookaMusic
2026-06-04 16:42:49 +02:00
parent 6c5c8e1c9b
commit 078d15e165
14 changed files with 518 additions and 153 deletions

View 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"]

View 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")
)
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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 |

View File

@@ -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 ===

View File

@@ -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");

View File

@@ -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) => {},

View File

@@ -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) ===

View File

@@ -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 ===

View File

@@ -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");
});

View File

@@ -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 |

View File

@@ -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");
});