Penetration Testing

Vanna.AI CVE-2024-5565: LLM Code Execution

How prompt injection in Vanna.AI reached a Python execution call in CVE-2024-5565, the vulnerability class behind it, and what to test in your LLM feature.

RG
Rathnakara GN
Cybersecify
11 min read

In May 2024, JFrog’s security research team disclosed CVE-2024-5565 in Vanna.AI, an open source Python library that turns a natural language question into a SQL query. The library also asked the model to write the Python charting code for the answer, and then ran that code. So a question could become a program. Nothing about the model was broken. The security decision sat one layer below it, where generated text was handed to an execution call with nothing in between. Any product where model output reaches an interpreter, a shell, a query engine, a template, or a browser has the same shape, whatever the model is.

Key findings

  • The record is CVE-2024-5565, published 31 May 2024, classified as CWE-94, Improper Control of Generation of Code, with a CVSS 3.1 base score of 8.1 assigned by JFrog as the CVE Numbering Authority. NVD had not published its own assessment at the time of checking.
  • The entry point is prompt injection. The finding is output handling. In OWASP Top 10 for LLM Applications terms, LLM01 gets in and LLM05 Improper Output Handling is what makes it matter.
  • The dangerous line is the sink, not the prompt. Generated code was executed. Remove or isolate that step and the same injection produces a bad chart instead of a bad outcome.
  • The mitigation named in the advisory is to disable the code generation path when the input comes from outside your trust boundary, rather than to filter the input.
  • The maintainer published a hardening guide in response, which is the pattern worth copying: document the trust boundary the library expects the integrator to hold.
  • Text to SQL products usually carry two sinks. The generated query is the one teams defend. The generated chart, template, or export step is the one that gets missed.

This post is written by Rathnakara GN, who leads AI and LLM penetration testing at Cybersecify. It analyses a publicly disclosed issue from the published record. We did not test Vanna.AI, and nothing here describes a live weakness or contains reproduction detail. The technique is kept at the level a defender needs in order to recognise the same shape in their own product.

What was disclosed

Vanna.AI is a Python library that answers questions about a database in natural language. It uses retrieval to assemble context about the schema, prompts a model to write SQL, runs the query, and then returns a result. To make the result readable it also produced a chart, and the charting code itself was generated by the model rather than written once by a developer.

The published mechanism is a short chain. The user’s question goes into the library’s ask method. The model writes SQL. The chart generation step then builds a second prompt that carries the original question and the generated SQL into it, and the Python it produces is passed to an execution call. The NVD entry states the consequence plainly: external input to the ask method with visualisation enabled leads to remote code execution.

The advisory’s mitigation is to turn the visualisation argument off when the input is untrusted, and the maintainer added a hardening guide for integrators.

That is all the intrusion detail this post needs. The rest is the part you can act on.

The part that matters: the sink, not the model

There is a reflex in most engineering teams to read an incident like this as a model problem, then to respond with a better model, a stricter system prompt, or an injection classifier in front of the input.

That reflex misplaces the boundary. A model produces text. It has no way to know whether the text it just produced will be shown to a human, stored in a row, or executed on a server. That judgement is made by the application, and the application made it here by calling an execution function on a string whose content was influenced by a user.

Read the same feature with the model removed and it becomes an old and familiar bug. Untrusted input reached a code interpreter. We have had a name and a fix for that class for twenty years. The model did not create a new vulnerability class, it created a new and much less obvious path into an existing one, because the string that reaches the interpreter now travels through a component that looks like a language feature rather than like data flow.

This is why the useful question after any AI incident is not what the model was tricked into saying. It is where the thing it said went next.

What to test in your own product

None of the following requires knowing anything about Vanna’s implementation. Work through it in order.

1. Draw the line where generated text stops being text

Pick one AI feature and follow a single response from the moment the model returns it to the moment the user sees it. Write down every function it passes through. Somewhere on that path the response either stays a string that gets rendered, or it becomes an instruction that something else acts on. That transition point is the only place in the feature where a security decision is being made, and in most codebases nobody has explicitly decided it.

2. Inventory every sink

Once you know what a sink looks like in your product, list them all. The recurring ones:

  • an evaluation or execution call in Python, JavaScript, or Ruby
  • a shell or subprocess invocation
  • a database query, including one assembled by an ORM
  • a template renderer, especially one with expression support
  • a markdown or HTML surface that can run script or load remote images
  • a file path, an archive extraction, or an upload destination
  • an HTTP client or webhook whose destination the model chose
  • any tool or function call the model may invoke with arguments it wrote

Chart and dashboard generation belongs on that list, and it is the entry most often absent, because it is filed mentally under presentation. Anything an engineer describes as letting the assistant write a quick script belongs there too.

A fast first pass is to search the codebase for the execution primitives in your language, then trace backwards from each hit to see whether model output can reach it. The list is usually longer than the team expects, and it grows quietly, because adding a generated artefact feels like a product improvement rather than a security change.

3. Prefer a fixed set of operations over generated code

If a feature needs to produce a chart, the chart types your product supports are a finite list. Generating fresh code each time buys flexibility that most products never use, and pays for it with an execution path that carries user influence.

The safer construction is to have the model choose from a set of operations you wrote, and to supply parameters that you validate, rather than to have it author the operation. That converts an open ended code generation problem into a closed one where the failure mode is a wrong chart rather than a wrong process.

The same reasoning applies to queries, filters, exports, and workflow steps. Let the model pick and fill in. Do not let it author what runs.

4. Assume injection succeeds, then bound what it reaches

Prompt injection cannot be reliably filtered out with current models, so any defence built on recognising a malicious instruction has a shelf life. Design as though the instruction eventually lands.

  • Run any generated artefact in an isolated environment with no network egress, a timeout, a memory ceiling, and no access to the host filesystem or environment variables.
  • Give the database user the narrowest privilege the feature actually needs. Read only is a start. Read only against the specific tables the feature reads is better. Scoped to the requesting identity, where the data model allows it, is better still.
  • Keep credentials and API keys out of the process that runs the generated artefact.
  • Require human confirmation for anything with a side effect: a write, a payment, an email, a permission change.

Each of these turns a full compromise into a contained failure. None of them depends on getting the prompt right.

5. Log the artefact, not just the answer

Most teams log the request and the final response. Almost nobody logs the intermediate thing the model produced. That intermediate artefact is the only record that shows what actually executed, and without it an incident review can establish that a user asked a strange question and cannot establish what the system then did about it.

Log the generated query, the generated code, the tool call and its arguments, and the identity that triggered it. Retain them long enough to be useful during an investigation. This is also the difference between finding out from your own telemetry and finding out from a customer.

Where this sits in the frameworks

Two categories, in sequence:

StageCategoryWhy
EntryLLM01 Prompt InjectionInstructions arrive inside content the model treats as part of its task
FindingLLM05 Improper Output HandlingModel output is passed into an execution context without validation or isolation
Underlying weaknessCWE-94 Improper Control of Generation of CodeThe classic name for the bug once the model is removed from the diagram

Reports that file this only under LLM01 tend to recommend prompt hardening, which does not close it. Filing it under LLM05 produces a remediation list you can finish. For the injection category in depth, see prompt injection 2026 attack patterns. For an incident where the same entry point led to a data exfiltration route instead of code execution, see our deep-dive on the Slack AI prompt injection disclosure.

Why pre-prompting is not a control

The JFrog write-up says it directly: developers should not rely on pre-prompting as an infallible defence.

The reason is structural. A system prompt is an instruction given to a component whose entire purpose is to follow instructions. It sits in the same context window as everything else, competing with the retrieved documents, the user’s question, and whatever else got assembled into the request. It is a strong preference, not a boundary.

Compare that with a sandbox with no network access, or a database role that cannot read the table in question. Those do not negotiate. When you are deciding where to spend a sprint on an AI feature, that is the distinction to spend it on.

What this means for an Indian SaaS company

The frameworks are jurisdiction neutral. The consequences are not.

If execution triggered through an AI feature leads to personal data reaching someone not authorised to see it, that is a personal data breach under the DPDP Act, and the disclosure path being a model rather than a database changes nothing about the obligation. Our DPDP breach response playbook covers the notification path.

The harder obligation is timing. CERT-In requires specified incidents to be reported within 6 hours of being noticed. For AI features the constraint is almost never the reporting, it is the noticing, and the gap closes at step 5 above rather than in the incident response plan.

What depth of testing finds this

Being specific about what different levels of assessment surface, because the difference is real work rather than a packaging exercise:

DepthWhat it surfaces here
Category coverageThe individual weaknesses. An injection surface on the input. A code execution call reachable from generated text. Both found, both real, both reported separately.
Systematic verificationWhether the boundary holds when exercised, rather than whether it exists on paper. This is where a sandbox that was configured but never had egress blocked gets separated from one that does.
Adversary emulationThe route. Input shaped at the generation step, producing an artefact that executes with the privileges of the service, reaching data the asking user was never entitled to.

Category coverage finds findings in isolation. Adversary emulation finds the path between them. Real incidents are almost always paths.

We do not claim that testing prevents any specific incident. The narrower and more useful statement is this: tracing model output to every sink it can reach, then checking whether crafted input changes the artefact that runs rather than the answer that returns, is the test that surfaces this class. If your product generates code, queries, charts, or templates from something a user typed, that trace is the test worth asking any vendor to perform, including us.

How Cybersecify tests this class

Cybersecify is a founder-led penetration testing firm based in Bengaluru serving AI-first and API-first SaaS startups. Rathnakara GN, Co-founder and CHO, holds OSCP and leads AI and LLM engagements.

Our AI application pentest maps the feature end to end: retrieval sources, the tool graph, the privilege the service holds, and every point where generated text is interpreted rather than displayed. Findings map to the OWASP Top 10 for LLM Applications 2025 codes and to CWE identifiers so an engineering lead or an auditor can cross reference each issue. Where the AI feature sits on top of an API, the API pentest covers the layer underneath it.

For the framework itself, see our OWASP Top 10 for LLM Applications reference, and for agent-specific extensions, AI agent pentest methodology.

Sources

Where to go from here

If your product generates code, SQL, templates, or charts from user input and you want to know whether that path is bounded, book a free 30-minute call with Ashok to scope the engagement, or see the pentest plans and pricing. For anything else, contact us.

We work with AI-first and API-first SaaS startups, Seed to Series B.

Frequently Asked Questions

What is CVE-2024-5565 in plain terms?

It is a code injection issue in Vanna.AI, an open source Python library that turns a natural language question into a SQL query using a large language model. Beyond generating SQL, the library also asked the model to write the Python charting code used to visualise the result, and then ran that generated code. Because the user's original question was carried into the prompt that produced the charting code, a question could influence the program that ran rather than only the answer that came back. The record was published on 31 May 2024 by JFrog as the CVE Numbering Authority, classified as CWE-94, Improper Control of Generation of Code, with a CVSS 3.1 base score of 8.1. The advisory's stated mitigation is to disable the visualisation path when the input comes from outside your trust boundary.

Was this a flaw in the language model itself?

No, and that distinction is the reason the incident is worth reading. The model did what models do, which is produce text that matches the instructions it was given. The security decision sat one layer below it, in the application code that took that text and passed it to a Python execution call. A model has no way to know whether the string it just produced will be displayed to a user or executed on a server. That judgement belongs to the application. Teams that read this as a model problem go looking for a better model or a stricter system prompt. Teams that read it as an output handling problem go looking for every place their application treats generated text as something other than untrusted data, which is the list that actually reduces exposure.

Which OWASP LLM Top 10 categories does this map to?

Two, and the order matters. The entry point is LLM01 Prompt Injection, because instructions arrive inside content the model treats as part of its task. The finding itself is LLM05 Improper Output Handling, because model output was passed into a downstream execution context without validation or isolation. Filing it only under LLM01 leads to the wrong remediation, since prompt injection cannot be reliably filtered out with current models. Filing it under LLM05 points at a fix you can actually complete: identify the sink, and either remove it, constrain what it can do, or stop generating the thing that reaches it. The 2025 edition of the OWASP Top 10 for LLM Applications renamed this category from Insecure Output Handling, so older reports may use the previous name.

What counts as a sink in an AI feature?

Any place where text produced by a model stops being displayed and starts being interpreted. The common ones are a Python or JavaScript evaluation call, a shell command, a database query, a template renderer, an HTML or markdown surface that can run script or load remote images, a file path, a webhook or HTTP client that chooses its own destination, and any tool or function call the model is allowed to invoke with arguments it wrote. Chart and dashboard generation is a sink that teams frequently miss, because it looks like a presentation feature rather than an execution feature. So is anything described internally as letting the assistant write a quick script. A useful way to build the list is to search your codebase for the execution primitives in your language, then trace backwards to see whether any model output can reach them.

Is a stricter system prompt enough to stop this?

No. The JFrog write-up makes the point directly, advising that developers should not rely on pre-prompting as an infallible defence. A system prompt is an instruction to a model that is designed to follow instructions, so it competes with every other instruction in the context rather than overriding them. Prompt hardening reduces casual attempts and raises effort, which is worth something, but it is not a control boundary. The controls that hold are architectural: do not generate executable artefacts from untrusted input, run anything you do generate inside an isolated environment with no network egress and a timeout, give the database user the narrowest privileges the feature needs, and require confirmation for anything with side effects. Those survive a model change. A prompt does not.

Our product uses text to SQL. What is different about our risk?

Text to SQL products usually carry two sinks rather than one, and teams tend to defend only the first. The generated SQL is the obvious one, and most teams have thought about read only database users and query timeouts. The second is whatever the product does with the result, which is frequently generated code for a chart, a summary passed into a template, or an export path. Vanna's issue was in the second. There is also a privilege question underneath both. If the connection the feature uses can read every table in the warehouse, then the worst case for a successful injection is not a broken chart, it is a broad read of data the asking user was never entitled to. Scope the database credential to the requesting identity where the data model allows it, and treat the chart or export step as its own review item rather than as presentation.

How would a penetration test surface this class of finding?

By tracing model output to every sink it can reach, then checking whether crafted input changes the artefact that runs rather than the answer that returns. In practice that means mapping the feature end to end, listing each point where generated text is interpreted rather than displayed, and testing the boundary at each one with instructions aimed at the generation step rather than at the visible reply. A category level sweep against the OWASP Top 10 for LLM Applications will report the sink and the injection surface as separate items. The route between them is found by chaining, which is a difference in depth rather than in packaging. If your product generates code, queries, or templates from user input, that trace is the test worth asking any vendor to perform.

Does an incident of this kind create obligations under Indian law?

It can. If code execution triggered through an AI feature leads to personal data being accessed by someone not authorised to see it, that is a personal data breach under the DPDP Act, and the path being a model rather than a database makes no difference to the obligation. The Act sets a maximum penalty of INR 250 crore for failure to take reasonable security safeguards and INR 200 crore for failure to notify the Data Protection Board and affected Data Principals. Separately, CERT-In requires specified cyber incidents to be reported within 6 hours of being noticed. The practical difficulty with AI features is the noticing. Most teams log the API request and the final answer, and never log the intermediate artefact the model produced, which is the only record that would show what actually ran.

Security questions, worries, or not sure what to use?

Cybersecify is a founder-led penetration testing firm for AI and SaaS startups. Tell us what you are weighing and we will give you a straight answer. Ask the team or book a free 30-minute call.

Share this article
AI SecurityLLM SecurityPrompt InjectionBreach AnalysisPenetration Testing

Spotted something wrong on this page? Facts change and we get things wrong. Tell us and we will check it. We publish corrections on the page rather than editing quietly.