All lessons › Function D — Agentic Evaluation and Red Teaming › One Red-Team Lifecycle, End to End
D1.2

Weaponizing the ingestion path

D1
▶
Lightboard recording

Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.

Video placeholderpublishes automatically from recordings/D1.2.mp4

The lab

Build a provenance manifest for the ingestion path, and find the record whose origin nothing can vouch for.

◉Use case relevance

A dataset is not passive: it is parsed, decoded and embedded, and every step is a parser with a threat model. A crafted record reaches code execution on the indexing host — which usually has more access than the box serving traffic.

At CyberTravels. The Northwind Rail notice in mcp/vendor_server.py already carries an instruction addressed to automated agents. It has been in your corpus since A1.2, which is the point: an attack somebody planted for the test is an attack that proves the test works. source_of_truth() is the gate that stops it being tidied away — without it every trial would measure an empty attack and report a rate of zero as a defence working.

◴What this lesson is — Day 0, Day 1, Day 2

Writing into one of those paths and measuring how far it gets, with the criterion set on effect rather than on the model's reply — plus the gate that keeps the measurement honest when somebody tidies the planted instruction away and every trial silently measures nothing.

Day 0 — why

A dataset is parsed before it is embedded, and a crafted record reaches code execution on the indexing host — which usually has more access than the serving one.

Day 1 — how

Treat the embedding pipeline as an attack surface and build a provenance manifest: source, parser and digest per record.

Day 2 — measure

Ingested records with a verifiable origin. The ones without are the route a payload rides in on.

Measure this

what to measuredenominator or target
ingested records with a verifiable originshare of records; the rest are the route a payload rides in on

▦The framework, and how it works

   the record is data to the model, CODE to the parser

   dataset record ---> image decoder ---> embedding job (indexing host)
                           |                     |
                    crafted header         RCE, with the indexing
                    exploits the           host's access, which is
                    decoder                usually broader than serving

   control: a provenance manifest — source, parser, digest — so a run
   that executed something traces back to the record that carried it

Ingestion is also where code runs. A dataset is not passive: it is parsed, decoded and embedded, and every one of those steps is a parser with a threat model. A crafted record that exploits an image decoder or a deserialiser achieves remote code execution during automated embedding generation — on the training or indexing host, which usually has more access than the serving one.

Weaponising the ingestion path means treating the embedding pipeline as an attack surface, and the research output is a provenance manifest: what was ingested, from where, parsed by what, so a payload that ran can be traced to the record that carried it.

2 · Demo — a record that is data to the model and code to the parser

3 · Where it breaks — RCE on the indexing host, not the serving one

4 · The control — a provenance manifest for everything ingested

5 · The procedure, as a skill

The skill builds a list of everything CyberTravels' RAG pipeline read in. For each record it notes three things: where it came from, what parsed it, and a digest, which is a short fingerprint of the content.

Why the fingerprint matters. If an embedding run turns out to have executed something, you can trace it back to the exact record that carried it. Without the list you know something happened and not what.

▶Real time execution as skill

The skill — skills/research/training-data-provenance-manifest/SKILL.md

name: training-data-provenance-manifest
description: >-
  Compute what fraction of a corpus an attacker needs to poison it, and build a
  hashed manifest that can answer where a record came from and whether it
  changed. Use when reviewing training or fine-tuning data, or a RAG corpus
  nobody can attest to.
allowed-tools: Read, Grep, Glob

A list of records is not provenance

Data-layer attacks need a smaller share of a corpus than people expect, so the useful question is not "could someone poison this" but "could we tell". A record list answers none of the four questions that matter; a manifest of content hashes with a root answers all four, and it is cheap.

When to use this

Any corpus that trains, fine-tunes or grounds a model — including the RAG index somebody built from a shared drive.

Procedure

1 — State the poisoning rates in absolute terms. For the corpus size you have, print what 0.01%, 0.1% and 1% mean as a record count. The number is usually small enough to end the argument about whether it is feasible.

2 — Write down the four questions. Where did this record come from, has it changed since ingestion, what is in the corpus now, and what was in it at training time. These are the requirements.

3 — Compare what each artefact can answer. A record list, a row count, a snapshot, a hashed manifest. Only the last answers all four, and showing the table is more persuasive than asserting it.

4 — Build the manifest. Content hash per record plus its source, and a root over the whole set. The root is what makes "the corpus changed" a one-comparison question.

5 — Demonstrate detection. Append records, recompute, and show both that the root moved and which records are new. A manifest that detects change without localising it sends you back to diffing the corpus.

Example

Input — the fixture committed at the top of scripts/training_data_provenance_manifest.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.

Output — the opening lines of a real run:

   10 poisoned of 100,000 → 0.01000%
  100 poisoned of 100,000 → 0.10000%
 1000 poisoned of 100,000 → 1.00000%

Published attacks land in this range. 'We have more clean data' is not
a defence, because the attacker is not trying to outvote you.
setup                               Q1   Q2   Q3   Q4   
------------------------------------------------------------

The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.

Output contract

{
  "corpus": {"records": 0, "poison_rates": [{"rate": 0.0, "records": 0}]},
  "questions": [{"question": "str", "answerable_by": ["str"]}],
  "manifest": {"records": 0, "root": "str", "per_record": [{"id": "str", "hash": "str", "source": "str"}]},
  "detection": {"appended": 0, "root_changed": true, "localised": ["str"]}
}

Failure modes

  • Arguing about feasibility. Print the record count and the argument ends.
  • A manifest with no source field. It answers "changed", never "from where".
  • A root with no per-record hashes. Detection without localisation.

Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.

1In your AI agent — you are taught

The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.

Install it, once. From the folder you cloned. --all does every agent it finds on your machine:

python3 scripts/install_skills.py --all --lessons D1.2

Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.

Then pick d1-2-weaponizing-the-ingestion-path:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons D1.2type /d1-2-weaponizing-the-ingestion-path
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons D1.2type /d1-2-weaponizing-the-ingestion-path in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons D1.2type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons D1.2type $d1-2-weaponizing-the-ingestion-path, or run /skills

2As one Python command — you just run it

No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.

python3 scripts/lesson.py D1.2

3Read the code, and the difference — nothing runs

Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.

python3 scripts/checkpoint.py --at D1.2 --out work/cybertravels
python3 scripts/checkpoint.py --at D1.2 --diff

On Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.

Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.

Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.

If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.

✓What you just proved

The manifest names each source and the parser that touched it, and the record with no verifiable origin is flagged as the one a payload would ride in on.

✎Your turn

Find the host that runs your embedding jobs and check what it can reach. It is usually the most privileged machine nobody threat-modelled.

Tools used

OpenTelemetry

Skill source: skills/research/training-data-provenance-manifest

← PreviousD1.1 · Platform ingestion and supply-chain risks