SecureAI-SCAN · docs
← MCP X-Ray tool
LLM01:2026 · Prompt Injection

How to detect prompt injection in code

Prompt injection isn't a keyword — it's a dataflow. It exists wherever user-controlled input reaches an LLM call in a position the model treats as an instruction. Here's how to trace that statically, and what a real finding looks like.

Short answer: trace data from a request source (HTTP body, form field, websocket message) forward through your code to see whether it lands in an LLM call's system-role message or gets concatenated into a system prompt string. If it resolves through a real import to a known LLM SDK and reaches that position unscoped, that's the vulnerability — independent of whether any specific attack phrase is present.

Why keyword matching doesn't work here

Scanning source code for phrases like "ignore previous instructions" catches text that's already sitting in a file — useful for checking untrusted content like an MCP tool description or a document you're about to feed to a RAG pipeline. It does not catch the underlying code vulnerability, because an attacker doesn't need to plant a famous phrase in your codebase — they need the code path to exist so that whatever they type at runtime lands somewhere the model treats as authoritative. The vulnerability is structural, not lexical.

What a traced finding looks like

A dataflow-based finding shows the actual path from an untrusted source, through however many function calls, to the exact LLM SDK call site it reaches:

HIGH  Prompt injection via user input
PROVEN  LLM01:2026 Prompt Injection

source  src/chat.ts:8   request data `req.body.input`
flow    src/chat.ts:13  passed as `systemPrompt`
sink    src/chat.ts:10  openai.chat.completions.create — system role (OpenAI)

fix     Keep system prompts static; pass user input as a user-role message.

Each line is checkable against the source file — that's what separates a traced finding from a keyword hit. Three things have to be true for this to count as a real (not heuristic) finding:

Trace it yourself

1. SourceFind where request-controlled data enters your code — an Express handler's req.body, a Next.js route's parsed form data, a webhook payload.
2. FlowFollow it through variable assignments, function parameters, and string concatenation — does it get merged into a prompt-building string anywhere along the way?
3. SinkCheck where it lands in the eventual LLM SDK call — a system-role message or a top-level system prompt is the dangerous position; a user-role message is the correct one.

Doing this by hand across a real codebase is slow and easy to miss — a value can pass through several functions before it reaches the sink. SecureAI-Scan automates exactly this trace via AST analysis (ts-morph for TypeScript/JavaScript, tree-sitter for Python), only flagging calls that resolve through real imports:

npx secureai-scan@0.9.0 scan .

Check an MCP tool description or config instead? That's a related but different check — invisible Unicode and injection phrasing in untrusted text, not code dataflow.

How to detect MCP tool poisoning →

Frequently asked questions

What is prompt injection?

Untrusted input reaching an LLM call in a position the model treats as an instruction — a system prompt or similarly elevated-trust position — instead of being clearly scoped as data. It's OWASP's LLM01:2026 category.

Does a "clean" scan mean the code is safe?

It means no traced dataflow from an untrusted source to a resolved LLM sink was found by the specific checks that ran. Static analysis is a filter, not proof of runtime safety — it can't see behavior that only emerges from how a model actually responds to a given input.

What's the fix?

Keep system prompts static; pass user-controlled input as a user-role message rather than concatenating it into the system prompt.