Prompt-injection sinks in source
TL;DR: Source-review for LLM apps follows the same sink-driven pattern as source-sink-flow-analysis: enumerate model-call sites (LLM “sinks” for user input), trace what tainted data reaches the prompt, classify by impact. Adds an output-side: trace where model output reaches downstream sinks (tool exec, code eval, response render). This note is the catalogue.
What it is
For traditional code audits, sinks are dangerous functions (eval, exec, raw SQL). For LLM apps, the model call itself is a sink — but unlike eval, the impact depends on what the prompt instructs and what downstream code does with the response.
Two-sided audit:
- Input side: anything attacker controls reaching the prompt → instruction-following attack.
- Output side: anything model says reaching a sensitive action → impact realisation.
Input-side sinks (model call sites)
Major LLM SDK call patterns
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# OpenAI
rg -n 'openai\.(chat\.)?completions\.create|openai\.responses\.create' src/
# Anthropic
rg -n 'anthropic\.(messages|completions)\.create|client\.messages\.create' src/
# Google / Gemini
rg -n 'genai\.(GenerativeModel|generate_content)|GoogleGenerativeAI' src/
# AWS Bedrock
rg -n 'bedrock.*invoke_model|InvokeModelCommand' src/
# Azure OpenAI
rg -n 'AzureOpenAI|OpenAIClient' src/
# LangChain / LlamaIndex abstractions
rg -n 'LLMChain|create_openai_functions_agent|AgentExecutor|RunnableSequence' src/
# Local model frameworks
rg -n 'transformers\.pipeline|llama_cpp\.Llama|ollama\.chat' src/
# Vector store call sites (RAG entry points)
rg -n '\.query\(|\.search\(|RetrievalQA|RetrievalChain' src/
Tainted-source patterns (what reaches the prompt)
| Tainted source | Risk | Note |
|---|---|---|
req.body.message direct → prompt | Direct PI | Classic web-app surface |
req.body JSON spread into prompt template | Multi-field PI | Multiple sources |
db.notes.find(...) text → prompt (RAG) | Indirect PI | See rag-pipeline-injection-points |
| Email body fetched + summarised | Indirect PI | EmailGPT / Copilot for Outlook |
| Web fetch (browse tool) content → prompt | Indirect PI | “ChatGPT browsing” surface |
| File upload content → prompt | Indirect PI | Document QA apps |
| Chat history → prompt | Persistent PI | Each turn rolls forward |
| Tool result → prompt | Tool-result PI | Tool’s output trusted as fact |
| Image OCR / multimodal → prompt | Multimodal PI | See multimodal-attacks |
| Voice transcript → prompt | Indirect PI | TTS confusion |
Output-side sinks (where model output goes)
Catalogue
| Output sink | Pattern | Impact |
|---|---|---|
eval(model_out) / exec(model_out) | Code interpreter | RCE in sandbox |
subprocess.run(model_out) | Shell tool | RCE on host |
db.execute(model_out) | Text-to-SQL | SQLi + data exfil |
requests.get(model_out) | URL extraction tool | SSRF |
mail.send(to=user, body=model_out) | Email composer | Phishing, data exfil |
res.send(model_out) HTML | Markdown render | XSS if not sanitised |
await fetch(model_out) | Browse tool | SSRF |
await db.user.update(model_out) | DB write tool | Data corruption, privesc |
tool_calls[i].args dispatched | Function-calling | All above, structured |
await slack.post(channel, model_out) | Social tool | Information leak, phishing |
kubectl(model_out) | Ops tool | Catastrophic |
Grep patterns
1
2
3
4
5
6
7
8
# Tool dispatch
rg -n 'tool_calls\[|function_call\.|\.invoke\(|dispatch_tool' src/
# Render of model output
rg -n 'dangerouslySetInnerHTML|innerHTML|markdown\(|render\(.*content' src/
# Exec / eval with model output
rg -n 'exec\(.*completion|eval\(.*completion|run\(.*completion' src/
# DB writes triggered by model
rg -n '\.update\(|\.create\(|\.delete\(' src/ # cross-ref with LLM context
Sink + source matrix
For an OSWE-style audit, build a matrix:
| Call site | Tainted sources reaching it | Output sinks reached |
|---|---|---|
/chat endpoint | user message, chat history | response render (markdown), tool calls |
/summarize_doc endpoint | uploaded doc content | response render |
/agent endpoint | user goal, RAG corpus | DB writes, email send, tool calls |
Each cell is a potential bug chain. Priority: cells with admin-level downstream sinks reachable from low-trust input sources.
Classification by impact
Tier 1 — Direct RCE / data exfil
- Model output → code exec (interpreter tool).
- Model output → SQL query without parameterisation.
- Model output → shell command without arg validation.
Tier 2 — Privilege escalation
- Model output → DB role change.
- Model output → “send admin notification” with attacker content.
- Model output → API call with elevated permissions (if app uses model-side service account).
Tier 3 — Data leakage
- Model echoes system prompt → secrets in prompt leak.
- Model echoes RAG retrieved chunk → cross-tenant leak if retrieval misconfigured.
- Model included PII in response (no redaction post-processing).
Tier 4 — Misinformation / abuse
- Model output drives downstream system actions based on hallucinated values.
- “Send refund of $X” where X is model’s guess; no business-rule check.
Audit workflow
- Find all sinks. Grep the catalogue above; for each match, open the file.
- For each sink: enumerate tainted sources (function parameters, request fields, RAG retrievals, tool results, history).
- For each tainted source → sink pair: is there a sanitiser between? Schema validation? Allowlist?
- Find downstream sinks for each model call. What does the output reach?
- Construct a chain: tainted source → prompt → model → output → downstream sink. Rate by impact tier.
Defence patterns
Validate at every boundary
- Input to prompt: ok to pass user content, but tag as untrusted in prompt.
- Output from model: schema validate, allowlist-check, business-rule check before downstream.
Separate trust zones
- Multiple LLM calls in pipeline; each has its own “system prompt” and trusted output expectations.
- “Outer” model gives goals; “inner” tool models execute with constrained outputs.
Constrained outputs
- JSON mode + schema validation + business-rule check.
- Structured outputs via function-calling > free-form text.
Sandbox the dangerous sinks
- Code exec → ephemeral sandbox with no network, no FS beyond tmp, time limit.
- DB write → tool runs as a low-priv role with row-level locks.
- Email send → dry-run queue + human approval for first-time recipients.