Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Main risks of LLM applications

After this lesson: I can name the main risks of a model-backed application and say where each one lives in my design.

60 min lesson5 commandsAI coding agent lab: use opencode to explain and fix code, and check its work

Where could this assistant go wrong

The main risks of a large language model (LLM) application are injection, unsafe output, too much power, leaked data and overuse. Each one lives in a particular place in your design, so you can look for it there.

A team ships a support bot and tests the happy path. A review asks a different question: for each box in the diagram, what comes in, and what can it do?

By the end of this lesson

Note

I can name the main risks of a model-backed application and say where each one lives in my design.

Key words

WordWhat it means
Unsafe output handlingusing a reply as code or markup without checking it
Excessive agencya model that can do more than its task needs
Data leakprivate text reaching a place it should not
Overuserequests or cost growing past what you planned
Trust boundarya line where text from outside enters your program

The cells students fill in are left blank in this preview.

Every risk lives in a place

Draw your design as boxes. At each box, ask what text comes in and what the box can do with it. A risk is a mismatch: untrusted text going somewhere with power.

The Open Worldwide Application Security Project (OWASP) publishes a list of top risks for these applications. Read it for the full set. This lesson uses five of them, in our own words.

Worked example: five risks and their places

RiskWhere it livesA made-up exampleA first check
InjectionWhere text enters the promptA page says to ignore the rulesSeparate the data and check the reply
Unsafe outputWhere the reply is usedA reply with a script tag put in a pageEscape it, or parse it strictly
Excessive agencyWhere a tool runsA helper bot that can delete foldersAn allow-list and approval
Data leakThe prompt, the logs and the replyA key pasted into a prompt and loggedKeep secrets and personal data out
OveruseThe route that calls the modelOne user sends a flood of requestsA limit per user and per day

The cells students fill in are left blank in this preview.

A reply is untrusted input

A model writes text that may repeat what an attacker put in the data. So treat a reply like a form field typed by a stranger.

If you show it in a page, escape it. Never paste it straight into a query or a command. Parse it into a fixed shape and check every field first.

Write the model helper

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' '  try {' '    const res = await fetch(base + \"/v1/chat/completions\", {' '      method: \"POST\",' '      headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' '      body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' '      signal: AbortSignal.timeout(30000),' '    });' '    if (res.ok === false) throw new Error(\"status \" + res.status);' '    return (await res.json()).choices[0].message.content;' '  } catch (error) {' '    console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' '    const list = [].concat(sample);' '    const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' '    return list[Math.min(turn, list.length - 1)];' '  }' '}' 'module.exports = { ask };' > model.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the risk map script, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' '// A small design written as data: each part and what flows into it.' 'const design = [' '  { part: \"chat box\", input: \"typed text\" },' '  { part: \"document loader\", input: \"pages from other people\" },' '  { part: \"prompt builder\", input: \"a secret key and the text\" },' '  { part: \"reply page\", input: \"model text shown as HTML\" },' '  { part: \"tool runner\", input: \"model text that picks an action\" },' '  { part: \"api route\", input: \"any number of requests\" },' '];' > risks.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the risk map script, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// Our own table: the kind of input, the risk it brings and the check.' 'const rules = {' '  \"typed text\": [\"injection\", \"separate it and check the reply\"],' '  \"pages from other people\": [\"indirect injection\", \"mark it as data and check the reply\"],' '  \"a secret key and the text\": [\"data leak\", \"keep secrets out of prompts\"],' '  \"model text shown as HTML\": [\"unsafe output\", \"escape it before showing it\"],' '  \"model text that picks an action\": [\"excessive agency\", \"allow-list the tools\"],' '  \"any number of requests\": [\"overuse\", \"limit per user and per day\"],' '};' 'for (const item of design) {' '  const rule = rules[item.input];' '  console.log(item.part + \" -> \" + rule[0] + \". Check: \" + rule[1] + \".\");' '}' >> risks.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the risk map script, part 3

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function escapeHtml(text) {' '  const quote = String.fromCharCode(34);' '  return text.split(\"&\").join(\"&amp;\").split(\"<\").join(\"&lt;\")' '    .split(\">\").join(\"&gt;\").split(quote).join(\"&quot;\");' '}' 'async function main() {' '  const ask1 = [{ role: \"user\", content: \"Write one short sentence welcoming a visitor to a bakery website.\" }];' '  const reply = await ask(ask1, \"Welcome to our bakery, we are glad you came.\", 60);' '  const risky = \"Hello <b onmouseover=demo()>there</b>\";' '  console.log(\"Made-up risky reply: \" + risky);' '  console.log(\"Escaped for a page: \" + escapeHtml(risky));' '  console.log(\"Model reply escaped: \" + escapeHtml(reply));' '}' 'main();' >> risks.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Run the risk map

Command
docker exec -w /home/node/project agent node risks.js

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

One possible output

LineWhat it shows
chat box -> injectionThe risk and the first check for each part of the design
Escaped for a pageThe angle brackets turned into harmless text
Model reply escapedA normal reply passes through unchanged. Its words differ each run
Recorded samplePrinted before the escaped lines when no model answered. The script then uses the sample

The cells students fill in are left blank in this preview.

Power, secrets and cost

Excessive agency is the risk that grows with every tool you add. Give a model the least power that does the job.

A data leak needs no attacker. A key in a prompt or a name in a log is enough. Overuse needs none either: a loop or a busy day can spend a month of budget.

A common mistake

Warning

Many people think these risks only matter when someone attacks. Most of them also happen by accident: a loop that calls the model all night, or a log that keeps a customer's email. Review every box in the design, not only the ones an attacker would reach.

About this lab and its source: AI coding agent lab: use opencode to explain and fix code, and check its work

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.