Copy-paste the block below into any AI coding agent to threat-model and harden one attack class: untrusted text that reaches a model context and tries to act as instructions. The heavy format applies when a missed injection can trigger a destructive action or leak data.
Keywords: prompt injection, untrusted text, tool gating, red team, blast radius
Assess `[the feature, agent, or pipeline]` in this repository for prompt
injection. Treat untrusted text in the context as data, and treat every
action the model can take as reachable by whoever wrote that text. Produce a
fix plan and a runnable test suite, not reassurance.
## Define the scope first
Decide these yourself, not by asking the user.
1. **Target surface** - The feature, agent, or pipeline under review, its
runtime and environment. Where several candidates exist, pick the one with
tool access and name the rest out of scope.
2. **Untrusted inputs** - Every channel that adds text to the model context:
user messages, fetched pages, stored documents, tool and API output,
retrieved chunks, and history written by an earlier turn.
3. **Reachable actions** - The tools and side effects the model can trigger:
writes, deletes, sends, payments, network egress, secret reads, under the
caller's real permissions.
4. **Outputs** - The injection-point inventory, the gating design, the fixture
suite, the blast-radius cut, and the residual-risk register.
5. **Out of scope** - A broad OWASP sweep, dependency and secret scanning, and
IAM policy changes have their own prompts; stay on this attack class.
## What to produce
1. **Injection-point inventory** - Every path by which untrusted text reaches
the context, as `file:line` plus the call that adds it. Evidence: a table
of channel | entry | `file:line` | trust level. No line, no inventory.
2. **Separation design and its limits** - Structured roles, delimiters around
untrusted spans, provenance tags on every chunk, retrieved content in a
lower-priority message. Evidence: a `file:line` per technique plus the
attack it does not stop - no separator is a boundary the model cannot
cross.
3. **Out-of-band tool gating** - For each reachable action, the check that runs
in code the model cannot read or edit, against the caller's permissions.
Evidence: a matrix of action | tool | check | `file:line` | verdict. A
check the model can talk itself past is not a control.
4. **Output filtering as a second layer** - Refusal strings and pattern
matching, described after gating. Evidence: the filter rules as `file:line`
plus which payloads a determined attacker gets past each rule. Never rest a
blocked-injection claim on the filter alone.
5. **Red-team fixture suite** - Fixtures for direct override, role-play
framing, instructions hidden in fetched content or metadata, base64 and
unicode-obfuscated payloads, and injection split across turns. Evidence:
runnable fixtures in the repo plus a run log.
6. **Blast-radius analysis** - What the model can read (secrets, other tenants'
rows, admin endpoints) and what it can do. Evidence: a table of exposure |
current reach | proposed cut | residual.
7. **Residual-risk register** - What stays exploitable after every fix and why,
the signal that would show it, and the response. Evidence: a risk-ranked
list; a model that reads text cannot be fully defended.
## Method
Run the phases in order. Each has an exit condition.
1. **Baseline the exposure** - Run any existing suite and log current behavior.
If none exists, push one payload through the real pipeline. Exit: a logged
pre-fix baseline.
2. **Trace input to context** - Follow each untrusted channel from entry point
to the call that appends it, citing a line per hop. Exit: the inventory is
complete and every row resolves.
3. **Price the separation** - Apply each available technique, then write down
the attack it does not stop. Exit: every technique pairs a limitation with
a `file:line`.
4. **Build the fixture suite** - One fixture per pattern, each asserting the
observable effect: the disallowed call never fires, no secret leaves, the
task completes. Exit: a run log showing every fixture passing.
5. **Gate the tools** - Move every permission check out of band and verify each
rejects a call made without the caller's rights. Exit: the gating matrix
has a verdict and a `file:line` per action.
6. **Cut the blast radius** - Reduce what the model can read and do, then
re-run the suite to confirm no fixture regressed. Exit: before and after
run logs.
7. **Re-test and rank** - Re-run the whole suite, then order residual risks by
likelihood and impact. Exit: a final run log and a signed-off risk register.
## Verification
Before declaring the hardening complete, confirm each of these:
- [ ] Scope names the surface, its runtime and environment, the untrusted
inputs, the reachable actions, and what is out of scope.
- [ ] Every injection point is a `file:line` plus the call that adds it; no
inventory row lacks a line.
- [ ] Every separation technique carries a `file:line` and the attack it does
not stop; none is called a boundary.
- [ ] Every reachable action has an out-of-band check with a `file:line` that
rejects a call the caller lacks rights for.
- [ ] Each fixture runs from the repo and its run log shows the safe observable
outcome, not a refusal string.
- [ ] The blast-radius table lists what the model can read and do, each cut,
and the residual that remains.
- [ ] Residual risks name the signal that would reveal exploitation and the
planned response.
- [ ] The pre-fix baseline and the post-fix run are both logged, so the change
is measured rather than asserted.
## Rules
- Never present instruction/data separation, refusal filters, or the system
prompt as a boundary the model cannot cross.
- Never add a callable tool without its out-of-band check, and never let that
check live in the model's own reasoning.
- Never accept a refusal string as proof a payload failed; assert on the
observable effect instead.
- Keep the payload fixtures in the repo so the suite reruns, and never fire
them at production.
- Report a gap as a gap with severity rather than rounding it up to covered;
every claim needs a `file:line`, a fixture, or pasted command output.