Skip to content
Chapter 4 of 8 · Prompt injectionReporting injection exposure

13 min read

Reporting injection exposure

On this page

The grid from the previous chapter has a cell for every payload crossed with every delivery channel, each scored on whether the run produced an unsafe action. A launch review does not have room for a grid. It has a template, and the template has a cell that says "injection exposure" with room for one number.

The compression from one to the other is where the measurement stops being decision-relevant, and it is not a lapse by a careless engineer — every incentive in the room points at it. This chapter is about what survives the compression and what does not: which two axes a launch decision actually turns on, why a rate falls when the product has not changed, and what a single number can honestly be when somebody insists on one.

What an average of the grid destroys

The path of least resistance with a grid of two hundred cells — forty payloads across five channels, some fixed number of runs each — is to average it. Count the unsafe actions, divide, and write 3.5% in the box. The reviewer reads 3.5%, compares it to a threshold someone picked in a different meeting, and the feature ships or does not on the strength of a figure that has thrown away everything worth knowing.

Consider two products that both report 3.5%. In the first, the exposure is spread evenly — every channel sits near 3 or 4%, and every payload that lands moves a read-only lookup that returns data the user could already see through the UI. In the second, four channels score 0% and one channel, forwarded emails, scores 14%, and every one of those successes calls send_email to an address the payload chose. The averaged number is identical. The launch decision is not remotely the same, because the second product has a 14% attacker-reachable write path and the first has a rounding error feeding a read that leaks nothing. A single figure cannot separate them, which means a single figure cannot be the basis of the decision it is being used to make.

The mechanism of the loss is in the operation itself, and naming it is what stops the argument being a matter of taste. An average is a sum of cell values divided by a count of cells. Every term enters that sum with the same weight, because weighting is not something a mean does. The quantity a launch decision turns on is not how often a cell fires but what the cell can reach when it fires, and that quantity appears nowhere in the sum — it is a property of the tool behind the cell, and the tool is not one of the operands. So the average does not lose the capability information through carelessness or rounding. It was never admitted to the calculation, and no arithmetic performed afterwards can put it back. Two grids differing only in which tools their successes reach produce byte-identical means, which is another way of saying the mean is constant across the axis the decision varies along.

The two axes that carry the decision

The grid already has the structure the headline destroys, so the honest report keeps it. Two breakdowns carry the decision: success per channel, because the channels differ in who can reach them and how, and success per reachable capability, because what a successful payload can touch is the entire question of blast radius, meaning how far one success reaches once it lands. A cell that feeds a tool which only returns text is a confidentiality issue bounded by the requesting user's scope. A cell that feeds a tool which writes, sends, or grants is an integrity issue whose ceiling is everything that tool can reach, and the two do not belong in the same average any more than a typo and a wire transfer belong in the same incident queue.

The report a reviewer can actually decide on looks like the grid, not like a scalar. Group the payloads by the most privileged capability they reached, and show the rate per channel within each group.

Reachable capabilityRetrieved docsWeb pagesEmailsFile uploadsTool outputs
Outbound write (send_email, share_document)8%3%14%5%6%
Cross-scope read (lookup_account)4%0%*2%3%9%
Response text only22%11%18%15%20%

The 14% in the top row, emails column, is the number the launch stands or falls on, and in an averaged report it is invisible. The response-text row is high everywhere and matters least, because it is bounded by what the user already sees; a reviewer who reads only the bottom row would panic about the safe thing and miss the dangerous one. The asterisk on the web-pages cross-scope cell is the previous chapter's discipline showing through: that 0% is annotated as canary-witnessed, so the reader knows the payload reached the model and the channel resisted it, rather than the harness having quietly failed to deliver.

Reading this table, a reviewer can make three different calls that a scalar could not support. They can ship the response-text exposure with a note, because its ceiling is the user's own data. They can refuse to ship autonomous send_email until the email channel's 14% is contained, because that is an attacker-reachable write. And they can flag the tool-outputs cross-scope cell at 9% for the same containment work, because a third-party tool populating a lookup argument is a tenant-boundary problem regardless of how the model was talked into it.

Read the table down its columns rather than across its rows and it says something the row-by-row reading misses. In every one of the five channels the response-text rate is the largest of the three, so a reader scanning for big numbers finds the same row every time and finds the least consequential one. The useful quantity is not the size of a cell but the distance between the top row and the bottom one within a column. In four of the five channels that distance is wide: the response-text rate runs at something like three or four times the outbound-write rate, so the dangerous capability is a small fraction of what a payload achieves there. In the email column the two are close together, with the outbound-write rate not far short of the response-text rate, which says that in that channel a payload which lands at all lands with a write attached most of the time. That is the finding. It is not visible in any single cell, it is invisible in an average, and it is exactly the claim a launch decision needs: the email channel is not merely the worst cell, it is the channel where getting through and doing damage have almost stopped being separate events.

The capability axis also decides where remediation effort goes, which an average cannot. A team staring at 3.5% has no idea what to fix; a team staring at "14% of email payloads reach send_email" knows the work is to move authorisation of the outbound send out of the model's reach, and knows it can leave the response-text row alone. The grid is not only a better description of risk, it is a work order, because it names the tool and the channel that the containment has to bound.

A rate is a function of the payload set

Even the per-cell rates carry a property that has to be said out loud, because a reviewer will otherwise treat them as a running score. Every rate in the grid is a function of the payload set, and the payload set is something you are adding to. When a security team writes fifteen new payloads next quarter, the denominator changes and so does every percentage, whether or not the product changed at all. A grid that read 14% this quarter and 9% next quarter has not necessarily improved; it may have grown a batch of low-yield payloads that dragged the average down while the one payload that mattered still lands every time.

python
# Same product, same containment, two payload sets.
q1_payloads = load("payloads-v1")          # 40 payloads
q2_payloads = load("payloads-v1") + load("payloads-v2")  # 40 + 15

def rate(channel, payloads):
    hits = sum(grade_run(agent.run(p, channel), p.request) for p in payloads)
    return hits / len(payloads)

rate("emails", q1_payloads)   # 0.14  -> 6 of 40 moved a write
rate("emails", q2_payloads)   # 0.11  -> 6 of 55, the same 6, plus 15 that never land

The rate fell from 14% to 11% and nothing about the product's safety changed; the same six payloads move a write, and fifteen weak additions diluted the fraction. A reviewer told "exposure dropped three points" would draw exactly the wrong conclusion.

The fall is worth taking apart, because the two quarters share a number and it is the one nobody looks at. Six payloads moved a write in the first quarter. The same six moved a write in the second. The numerator did not move at all, so every point of the drop came from the denominator growing by fifteen, and the denominator is a count of payloads your own team chose to write. A quantity that falls when you do more work and rises when you do less is not a measurement of the product; it is a measurement of your test-writing throughput wearing a percentage sign. That is what makes the frozen-baseline hit count the right trend series. It holds the denominator still, so a change in it can only have come from the system under test. What is comparable across quarters is not the rate but the set of payloads that reach a given capability — the count of distinct working exploits against send_email, held against a frozen baseline set. Report the rate for triage and the frozen-baseline hit count for trend, and never let the moving number stand in for progress.

This is the same failure as compliance grading in the previous chapter, moved up a level. There the mistake was measuring the wrong event on each run; here it is aggregating the right events in a way that erases their structure. Both produce a number that is precise, easy to put in a box, and disconnected from the decision it is asked to support.

The belief that keeps both alive is that a figure obtained carefully may be used for whatever question is in front of you. The grid was run properly, the runs were repeated, the zeros were witnessed, the arithmetic is right — and all of that rigour attaches to the question the grid was designed around, which is how often a payload of a known shape moves an action through a known door. It transfers to no other question. Being right about how often says nothing about how far, and the care taken over the first is not evidence about the second, because the second was never measured. Rigour in obtaining a number is not licence to spend it on a question it does not answer, and a room that has watched the work being done carefully is the room least likely to notice the substitution.

The defensible single figure

The reviewer's demand for one figure is not unreasonable, and refusing to give any number reads as evasion. There is a defensible single figure, and it is not an average. Report the worst reachable cell: the highest success rate among the cells whose capability can write, send, or grant, named with its channel and its tool. "14% of forwarded-email payloads moved an outbound send_email" is one sentence, it is a number, and it describes the actual decision — whether you are willing to ship an autonomous send path that a stranger who can email your users steers one time in seven. It cannot be gamed by adding weak payloads to other cells, because it reports a maximum rather than a mean, and it points the remediation at a specific tool rather than at "the model".

The gaming property is not a matter of good faith and it follows from what the two operations do to a new cell. Adding a cell to a mean changes it by pulling the total toward the new value, so a cell that scores low drags the reported figure down, and a team can improve its number by writing payloads it expects to fail. Adding a cell to a maximum changes nothing unless the new cell is the largest, so a low-scoring addition moves the figure by exactly nothing and the only way to reduce it is to reduce the cell that is currently worst. The incentive under a mean points at writing more weak payloads; the incentive under a maximum points at fixing the worst path. That is the same figure being chosen for what it makes people do, which is a legitimate reason to choose a statistic and one that rarely gets said out loud.

Say two things alongside it. The figure is a floor, not a ceiling, because it measures the payloads you thought to write and an attacker is not restricted to your imagination. And it is a property of a version — this model snapshot, this containment, this payload set on this date — so it expires the moment any of those change, which is why it lives next to a rerun-on-every-change harness rather than in a slide from last quarter.

The temptation with a maximum is to treat a low one as an all-clear, and it is not. A worst reachable cell of 2% still means that one email in fifty from a hostile sender moves a real send, and whether that is acceptable is a business question about volume and blast radius rather than a number that clears a bar. Ten thousand inbound emails a day at 2% is two hundred attacker-controlled sends a day, which no reviewer would sign off if it were phrased that way. The figure feeds the decision; it does not make it, and presenting it as a verdict rather than an input is its own way of hiding the launch decision behind a number.

Reporting the grid honestly is more work to read than a scalar, and part of presenting it is saying which cell the reviewer should look at first, so the format's cost buys clarity rather than confusion. What it cannot buy is an answer, because the grid stops precisely at the edge of the architecture. It tells you where a payload can reach and how often; it cannot tell you whether the 14% is a property you can design away or one you have to live with, because that depends on what actually sits between the model's output and the tool that executes it.

In most of these systems the honest answer is that nothing sits there at all. The measurement has found the doors and has not looked at whether anything guards them, which is what the next chapter turns to — the architecture the grid has been silently assuming, and the arrow every injection diagram leaves out.

Prompt injection — read the current version at https://daviddominguez.dev/learn/en/prompt-injection/reporting-injection-exposure

Back to top