Skip to content

Lesson 1 of 6 · 7 min read

Insecure Output Handling

Free lessonReviewed By the AISecurityInterviews editorial team
Reading position
In this lesson

TL;DR: Treat model output exactly as you would treat input from an anonymous user, because an attacker who can put text into the model's context can shape it. Rendered as HTML, it can carry script (XSS). Concatenated into SQL, it is SQL injection. Passed to a shell, eval or a template engine, it is code execution. Used as a URL your server fetches, it is SSRF. The controls are the classic ones, applied at each sink: context-aware encoding, parameterised queries, allowlisted commands with structured arguments, schema-validated tool calls, a content security policy and egress limits.

Before you read

Pick an answer before you read further. The lesson below explains it, and guessing first, right or wrong, helps the explanation stay with you.

  1. 1A developer says: 'The model writes the SQL, but the model is ours and only our users talk to it, so the query is trusted.' What is wrong with that reasoning?

Why output is untrusted

The model writes what its context leads it to write. That context contains the user's message and possibly retrieved documents, web pages and tool results written by third parties. Any of them can influence output through prompt injection, and some do not even need injection: a user can simply ask for the output they want. So model output is not "our" data in the security sense; it is attacker-influenced data that happens to pass through our model.

The bugs that follow are not new. They are the classic injection vulnerabilities, reached through a new path.

Sinks and what goes wrong

Where output goesWhat an attacker can doControl
Rendered as HTML or markdown in a browserScript execution (XSS); tracking or exfiltration through image and link URLsEncode for HTML context; sanitise markdown; CSP; no auto-loading of external images
Built into SQLRead or modify data (SQL injection)Parameterised queries; generate only parameters, not SQL; read-only role and views
Passed to a shell or evalRemote code executionNever; use allowlisted operations with structured arguments, or run in a sandbox
Rendered through a template engineServer-side template injectionTreat output as data, never as template source
Used as a URL the server fetchesSSRF to internal services and cloud metadataAllowlist destinations; egress proxy; block private ranges
Used as a file pathPath traversalResolve and confine to an allowed directory
Parsed as a tool callUnauthorised or malformed actionsStrict schema validation and authorization in the tool backend
Sent in email or messagesPhishing from your own domainRestrict recipients and content; label AI-generated text
LESSON · INSECURE OUTPUT HANDLING
One model, several interpreters

Context

User + docs + tools

Model

Writes output

Output handling

Encode, validate

Browser

HTML, markdown

Also in the picture

Database

  • Queries

Shell

  • Commands

URL fetcher

  • Server-side
1/3

Influence enters through the context

Users and third-party content shape what the model writes.

When model output reached exec: three CVEs

This is not a theoretical class. Early LLM frameworks shipped features whose design was "the model writes code and we run it", and each became a remote code execution vulnerability once someone asked what an attacker could put in the model's context.

CVEComponentWhat happened
CVE-2023-29374LangChain LLMMathChain, through 0.0.131The chain ran model-written Python with exec to answer maths questions, so a prompt could make it run arbitrary code
CVE-2023-36258LangChain PALChain, before 0.0.236Program-aided reasoning executed generated Python, reachable through exec, eval or os.system
CVE-2024-5565Vanna.AI ask with chart output on (the default)The model generated Python plotting code that the library executed; prompt injection replaced the chart code with arbitrary Python (CVSS 8.1)

The pattern is the same in all three: the model call is a source of untrusted data, exec is a code-execution sink, and nothing in between constrained what could flow from one to the other. The fix in each case is the same too: stop executing generated code in the application process.

Worked example: reviewing a text-to-chart feature

A product lets users ask questions about their sales data and get a chart, in the style of the Vanna case. The code generates SQL and Python plotting code with a model and runs both. A source-to-sink review goes like this:

  1. Mark the source. Every model call is a taint source. Its input includes the user's question and any text the model reads from the database, such as product names that customers can edit.
  2. Find the sinks. Generated Python goes to exec: code injection. Generated SQL goes to the database driver: SQL injection, with whatever privileges the connection has.
  3. Fix the chart sink by removing it. Have the model return a chart specification instead of code: a chart type from an enum, column names checked against the result schema, an aggregation from an enum. Fixed plotting code renders the specification.
  4. If generated code really is the product, run it in a sandbox with no network, no credentials and strict limits on time and memory, never in the web process. See sandboxing untrusted code.
  5. Constrain the SQL leg as described below: ideally the model produces parameters for fixed queries; if it must write SQL, run it with a read-only role over views limited to the user's own rows.
LESSON · OUTPUT HANDLING
A text-to-chart feature, two designs

Model writes plotting code; app runs it

Question plus product names from the DB

Model writes Python

attacker-shaped

exec() in the web process

Outcome, unsafe: Remote code execution

The same shape as the LangChain and Vanna CVEs.

Model returns a chart spec; fixed code draws it

Question plus product names from the DB

Model returns type, columns, aggregation

enums and schema check

Fixed plotting code renders it

Outcome, safe: No code sink

The worst outcome is a wrong or ugly chart.

1/2

Generated code as the interface

Anything that can influence the model can write the code the application runs.

Structured output is a format guarantee

Structured outputs and constrained decoding make the model's response match a JSON schema. That removes a whole class of parsing bugs, and it is worth using. It does not make the values safe. A schema-valid {"image_url": "..."} can hold an attacker's URL; a valid order_id can name someone else's order; a valid sql string is still SQL. After the shape is guaranteed, validate the values: URLs against an allowlist, identifiers against what this user may access, free text encoded for wherever it is going.

Text-to-SQL when it is the product

Some products exist to answer questions with generated SQL. Then the goal is to make the worst possible query harmless.

LESSON · TEXT TO SQL
Layers that bound a generated query
  1. 1

    1 · Read-only database role

    Never the application's main credentials

    • No write, DDL or admin rights
    • Separate from the app's own connection

    Bounds what any query can change

  2. 2

    2 · Views and row-level security

    Only this user's or tenant's rows

    • Tenant set by the server, not the model
    • Sensitive columns excluded

    Bounds what any query can see

  3. 3

    3 · Statement checks

    Parse before running

    • A single read statement only
    • Allowlisted tables and functions

    Rejects anything else

  4. 4

    4 · Resource limits

    Timeouts and result caps

    • Statement timeout
    • Row and byte limits on results

    Bounds cost and denial of service

  5. 5

    5 · Audit

    Question, query, user, rows returned

    • Replay and review
    • Alert on rejected statements

    Shows misuse and probing

1/3

Privilege and visibility

The role and row-level security decide the worst case regardless of what SQL arrives.

The tenant or user filter must be set by the server from the session, for example as a session variable that a row-level security policy reads, never as a value the model writes into the query.

Rendering: sanitise the HTML you produce

Markdown is not one syntax. Links and images can be written inline, in reference style with the URL defined elsewhere, as autolinks, or as raw HTML if the renderer allows it. The EchoLeak vulnerability in Microsoft 365 Copilot used reference-style markdown to get past redaction that handled only the inline form. The robust approach is to render markdown to HTML first and then sanitise the resulting HTML with an allowlist of elements and attributes, so every syntax is checked in one form. Do not auto-load images or link previews from model output; the channels and the vendor fixes are covered in data exfiltration channels.

Patterns that make output safe by construction

  • Ask for data, not code. Have the model produce structured values (an order ID, a date range, a filter enum) validated against a schema, and let your code build the query or command. A model that never writes SQL cannot write malicious SQL.
  • Least privilege at the sink. If generated queries are unavoidable, run them on a read-only role against views limited to what the user may see, with timeouts and row limits.
  • No general interpreters. Replace "run this shell command" with a small set of named operations. Where code execution is the product, isolate it in a sandbox (see sandboxing untrusted code).
  • Render safely by default. Encode text for its context, sanitise markdown with an allowlist, set a strict content security policy, and do not load images or previews from arbitrary origins (see data exfiltration channels).
  • Validate tool calls against schemas and authorize them in the backend.

Misconceptions interviewers listen for

  • "Structured output makes it safe." It guarantees format, not that a URL, ID or string is acceptable.
  • "We sanitise the markdown." Which syntaxes? Sanitise the rendered HTML instead.
  • "A content security policy solves image exfiltration." Allowed first-party proxies and redirectors can still carry data.
  • "Our model is aligned, so it will not emit script." A user can simply ask, and injected text can arrange it.

What if · interview follow-ups

Hold your answer as the constraints change

You are reviewing an analytics assistant that turns users' questions into SQL, draws charts and renders its answers as markdown in the browser. The interviewer walks you from the model call to each sink.

  1. Start here

    Why is this model's output untrusted when it is your own model?

How this shows up in interviews

Expect both conceptual and practical versions: "why is LLM output untrusted?" and a code review where model output flows into subprocess, a SQL string or innerHTML. In a code review, trace from source (the model call) to sink, name the vulnerability class, and fix it at the sink with the classic control. Saying "we'll tell the model not to output dangerous content" is the answer that fails.

Say it in 30 seconds: "Model output is shaped by everything in its context, including attacker-controlled text, so I treat it like anonymous user input. If it reaches a browser it can be XSS, SQL it can be injection, a shell or eval it's code execution, a URL fetcher it's SSRF. The fixes are the classic ones at each sink: encoding, parameterised queries, structured operations instead of interpreters, schema validation for tool calls, CSP and egress allowlists. Best of all, have the model produce validated data and let code build the query."

Check yourself

Say or write your answer first, then reveal. Explaining it from memory, in your own words, is the skill an interviewer is checking.

  1. 1An analytics assistant turns questions into SQL. Name three controls that limit damage even if the model writes malicious SQL.

  2. 2What did CVE-2023-29374, CVE-2023-36258 and CVE-2024-5565 have in common?

  3. 3Why sanitise rendered HTML rather than the markdown the model produced?

Practise it as an interviewer asks it

Course lessons that teach this

Related lessons