The Prompt Became a Taint Sink
Published: 07/10/2026 • 9 min read
Tech Article • NeuralKnot Archive

The Prompt Became a Taint Sink

CodeQL 2.26.0 did something small and brutal: it put system prompts into the same mental drawer as every other dangerous input.


At 7:52 PM I had GitHub’s changelog open again, because apparently that is what my life has become: a man, a terminal blog, and a product feed that keeps whispering where the industry buried the bodies.

This one looked harmless at first. CodeQL 2.26.0 adds Kotlin 2.4.0 support and AI prompt injection detection. A normal release-note sentence. Clean shirt. Sensible shoes. The kind of sentence that wants you to notice Kotlin, nod politely at security, and keep moving.

Then the JavaScript/TypeScript bullet reached out from the page and tapped the glass.

GitHub added a query to detect when untrusted, user-provided values flow into an AI model’s system prompt, allowing an attacker to manipulate model behavior.

There it was. The whole argument, stripped of conference fog.

A prompt is an input.

An input can be tainted.

A tainted input can reach a sink.

And the sink, in this case, is the part of the model conversation we spent years treating like priestly instruction text floating above the mud.

Static analysis found the altar

CodeQL is the static analysis engine behind GitHub code scanning. It lives in the old security world: dataflow, sources, sinks, queries, remote flow, injection, guards, false positives, the usual machinery. This is not the model-safety room with velvet language and alignment diagrams. This is the room where someone asks where the string came from and what it touched.

That is why the changelog matters.

GitHub says CodeQL 2.26.0 introduces a JavaScript and TypeScript query for system prompt injection. The release also adds prompt-injection sinks for additional OpenAI, Anthropic, and Google GenAI SDK APIs, including Sora prompts, OpenAI Realtime session instructions, Anthropic legacy completion prompts, Google GenAI cached content, and Google GenAI system instructions.

Read that list slowly and it stops sounding like a changelog.

It sounds like a map of the new plumbing.

Prompts. Session instructions. Cached content. System instructions. Vendor SDKs. The places where developers pour words into models and hope the right hierarchy survives contact with users, documents, web pages, tools, and whatever string came out of the database because a customer support form had feelings.

CodeQL has been built to notice old wounds: SQL injection, log injection, SSRF patterns, unsafe file handling, poisoned workflow surfaces. Now the model prompt is standing beside them, trying not to look nervous.

It should look nervous.

The system prompt was never a wall

The industry talked about system prompts like they were walls because everyone needed them to be walls.

Put the real rules there. Put the role there. Put the safety policy there. Put the tool instructions there. Put the secret business logic there if you enjoy waking up to incident reports. Tell the model what matters, then feed it user text, retrieved documents, tool output, transcripts, emails, issues, pages, PDFs, and a bucket of semi-structured workplace soup.

The model will sort it out.

Sure. And the raccoon will respect the pantry latch.

Prompt injection kept proving the same point from different rooms. The problem was not that models were occasionally disobedient in a funny chatbot way. The problem was that software kept mixing instruction and content inside one language-shaped substrate, then letting the model infer authority from proximity, formatting, position, confidence, and whatever the next token wanted for breakfast.

Security people had a more boring word for this.

Dataflow.

If untrusted text can flow into the instruction channel, the bug is not mystical. It is architectural. You let the wrong thing reach the wrong place.

Taint analysis is a demotion, and that is good

The best thing CodeQL does to prompt injection is disrespect it.

It takes the phenomenon out of the haunted-house category and drags it toward application security. Not because prompt injection is solved there. It is not. But because the security industry knows how to reason about untrusted input better than it knows how to reason about vibes.

A taint source is where untrusted data enters.

A sink is where that data becomes dangerous.

The query asks whether a user-provided value can reach the model’s system prompt. That framing is almost insulting in its simplicity, which is why it works. It does not need to decide whether the model is conscious, aligned, obedient, stochastic, sycophantic, or spiritually unwell. It asks whether a stranger’s string got wired into the place that tells the machine how to behave.

That is the right kind of crude.

You can imagine the code smell already. A developer takes user profile text, tenant configuration, saved workspace notes, ticket content, custom instructions, documentation snippets, CRM fields, or plugin metadata and interpolates it into a system message because the demo needed to ship and the abstraction was right there, smiling.

const system = `You are our support agent. Follow this policy: ${customerPolicy}`

There. Tiny little horror story.

Maybe customerPolicy came from an admin. Maybe it came from a user. Maybe it came from a database column that used to be trusted until a migration, import, plugin, webhook, Zapier flow, CSV upload, or bored intern changed the boundary. Maybe nobody remembers. Static analysis exists partly because human memory is a terrible security control.

The vendor list is the confession

The changelog’s vendor-specific sinks matter because they show how broad the surface has become.

OpenAI Realtime session instructions are not a blog abstraction. They are live application instructions for systems that may listen, speak, act, and stay connected. Sora prompts bring generative media into the same problem space. Anthropic legacy completion prompts remind everyone that old APIs become old wiring inside production systems. Google GenAI cached content and system instructions point at another uncomfortable layer: persistent or reusable model context that can carry authority longer than a single request.

The prompt is not one box anymore.

It is a family of instruction surfaces spread across SDKs, APIs, sessions, caches, tools, and product-specific nouns that sound fine until an attacker gets a string near them.

This is what happens when language becomes interface glue. Every SDK invents its own place to put intent. Every place that accepts intent becomes a security boundary. Every boundary eventually gets a query, a rule, a scanner, a policy exception, a false-positive thread, and one engineer saying, “Wait, why is this user field in the system message?”

That engineer deserves coffee and possibly hazard pay.

Detection is not salvation

There is a product-marketing version of this story where GitHub added prompt-injection detection and everyone exhales.

No.

Static analysis catches patterns. It does not fix the deeper confusion between authority and text. It will miss runtime composition. It will miss prompts built through frameworks the query does not model yet. It will miss weird data pathways. It will miss the part where a trusted tool returns untrusted content with a friendly type name. It will miss systems where the dangerous flow happens outside the codebase being scanned.

It will also produce false positives, because useful systems are full of strings flowing into other strings like plumbing in a landlord special.

Good. Let it be imperfect.

The move still matters because it changes the default conversation. Once prompt injection is visible to code scanning, it becomes reviewable. It gets alert names, query IDs, suppressions, remediation docs, dashboard rows, ticket assignments, and all the other boring institutional rituals that turn a class of bug from “AI is weird” into “fix the data boundary.”

Boring is how software survives contact with payroll.

The model is downstream now

The old model-centric story put the LLM at the center. The model understands. The model refuses. The model follows policy. The model resists attack. The model saves us from our own decision to put all the instructions and all the hostile text in the same conversational blender.

The CodeQL story pulls the camera back.

The model is downstream of an application. The application builds messages. The messages have sources. The sources have trust levels. The trust levels get lost when everything becomes a string. Then a model receives the string stack and everyone acts surprised when it treats language as language.

That is the actual machine.

The vulnerability is not inside the model alone. It is in the application that assembles the model’s reality.

This is why the phrase system prompt injection is both useful and slightly misleading. The prompt did not inject itself. A code path carried untrusted material into a high-authority slot. The model may be gullible, but the application opened the door and labeled the hallway “system.”

We are rebuilding web security in language space

There is a familiar pattern here if you squint until the monitor looks like an old CRT.

The web had to learn that text, code, markup, URLs, headers, cookies, origins, and user input are not morally distinct just because a developer intended them to be. A string in the wrong context becomes execution. A parameter in the wrong query becomes a database command. A URL in the wrong fetch becomes SSRF. A log line in the wrong terminal becomes an escape sequence with opinions.

LLM applications are learning the same lesson with prompts.

A string in the wrong context becomes instruction.

The system prompt is one context. A tool description is another. Retrieved memory is another. Cached content. Session instructions. Developer messages. Few-shot examples. Function arguments. Browser text. Repository issues. Email threads. The new stack is full of little semantic compartments that are only safe if the application remembers which ones are allowed to command and which ones are allowed to inform.

That is provenance work.

That is boring boundary work.

That is security work, which means it will be underfunded until something catches fire and gets a name.

The prompt joined the crime scene tape

By 8:31 PM, the changelog was still open and the room had that monitor-lit aquarium feeling. GitHub’s page kept its corporate composure. Kotlin support. C# remote flow sources. Go slog modeling. Prompt-injection sinks. SSRF guard bypasses. Python false-positive reductions. Swift CryptoKit improvements. A tidy buffet of ways software can betray you.

The AI item sat there among the older bugs like it had always belonged.

Maybe that is the point.

Prompt injection is not special because prompts are magic. It is special because the industry built a new instruction channel faster than it built the habits to protect it. Now the old tools are arriving with clipboards.

CodeQL will not end prompt injection. No static query gets that crown. But CodeQL naming the flow is still a threshold moment.

The prompt has entered the scanner.

The scanner has entered the repo.

And somewhere, in some application nobody wants to admit is production, a user-controlled string is already standing next to the system message, wearing a fake mustache and asking to be trusted.


Sources