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

View File

@@ -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%)**