Lesson 15 / 22
Injection Attacks
Measuring the injection class as a six-step chain: which step each of the six defense layers breaks, the absence of a control in forty-two of the seventy-two step-site combinations across twelve call sites, the three-layer paper design being complete at only one call site, six of the one hundred ninety-two input-site combinations completing all six steps, and those six concentrating at two call sites.
Published Updated
In this lesson
- Models the injection class as a six-step chain and writes out each step's precondition
- Runs the same input set through the defense layers and counts which step each layer breaks
- Measures the breakable link per call site and gives the cost of breaking late in items
Contents
The previous lesson covered ten risk items across three columns and gave the injection class the highest late-break cost: a six-step chain, a control point at three steps, 480 fictional items if never broken. This lesson opens up that row. Each of the six steps is written with its precondition, the six defense layers are mapped to steps, and the same input set is run through the fictional measurement network’s twelve call sites.
The measurement runs entirely on formal properties. No input value is written out, no query text is constructed, and no fix example is given. Inputs are defined only by four binary properties: does it comply with the field contract, does it carry that context’s delimiter character class, does the context produce a return, is its length long. What is measured is not the input itself, but which step the input stops at.
The Chain’s Six Steps
Injection is not a single moment; it requires all six steps to hold in sequence.
- Entry. The value enters the system through an endpoint. Precondition: the endpoint is reachable from outside and the field accepts this value.
- Concatenation. The value is written into the text that will be handed to an interpreter. Precondition: the call site does not pass the value as a separate operand.
- Parsing. The interpreter reads part of the value as syntax, not data. Precondition: the value carries that context’s delimiter character class and that class has not been escaped.
- Authority. The altered expression runs with the calling identity’s privileges. Precondition: that identity can access the targeted items.
- Return. The result reaches the caller. Precondition: the response body or error text carries detail and the context produces a return.
- Exit. The obtained items are carried out. Precondition: there is no limit on the exit volume and destination.
None of the steps was written with how it is done; each was written with what it needs. That is exactly the defense’s job: to remove a step’s precondition.
TA6: the step sequence and the cost field are a model; cost is the number of fictional
items affected if the chain passes that step unbroken. TA7: each layer breaks a single
step; a conditional layer breaks only for a specific input class. TA8: step 3 holds only
if the input carries the delimiter class, step 5 only if the context produces a return.
TA9: the same input is run separately through every call site; a chain broken at one site
is not counted as broken at another.
// inj/chain.mjs — the injection class's step sequence and defense layers (model). // There is no real system, no real endpoint, and no real input value; inputs are // defined only by their formal properties. export const STEP = [ { n: 1, name: "entry", cost: 0 }, { n: 2, name: "concatenation", cost: 0 }, { n: 3, name: "parsing", cost: 40 }, { n: 4, name: "authority", cost: 120 }, { n: 5, name: "return", cost: 300 }, { n: 6, name: "exit", cost: 480 }, ]; export const LAYER = [ { name: "field-contract", breaks: 1, cond: (g) => !g.contract, legit: 3 }, { name: "separate-operand", breaks: 2, cond: () => true, legit: 0 }, { name: "context-escaping", breaks: 3, cond: () => true, legit: 1 }, { name: "least-privilege", breaks: 4, cond: () => true, legit: 6 }, { name: "plain-response", breaks: 5, cond: () => true, legit: 4 }, { name: "exit-limit", breaks: 6, cond: () => true, legit: 9 }, ]; export const PRECOND = { 3: (g) => g.delimiter, 5: (g) => g.readback }; // 16 fictional inputs: all combinations of four binary properties. No value is ever written out. export const INPUT = []; for (const contract of [true, false]) for (const delimiter of [true, false]) for (const readback of [true, false]) for (const long of [true, false]) INPUT.push({ no: INPUT.length + 1, contract, delimiter, readback, long }); export const classify = (g) => !g.contract ? "off-contract" : g.delimiter ? "delimiter-bearing" : g.long ? "long-but-compliant" : "plain-compliant"; // The fictional measurement network's 12 call sites and the layers actually deployed at each. export const CALL = [ ["subscriber-portal.search", ["field-contract", "separate-operand", "context-escaping", "plain-response"]], ["subscriber-portal.billing", ["field-contract", "separate-operand", "plain-response"]], ["subscriber-portal.dispute", ["field-contract", "separate-operand", "context-escaping"]], ["collector.read-write", ["separate-operand", "least-privilege"]], ["collector.bulk-transfer", ["separate-operand", "least-privilege", "exit-limit"]], ["validator.threshold", ["field-contract", "separate-operand"]], ["validator.archive-search", ["field-contract"]], ["billing-generation.period", ["separate-operand", "plain-response", "exit-limit"]], ["billing-generation.report", ["context-escaping", "plain-response"]], ["work-order.create", ["field-contract", "separate-operand", "least-privilege"]], ["work-order.field-note", []], ["branch-endpoint.query", ["field-contract", "separate-operand", "context-escaping", "least-privilege"]], ]; export const write = (g, ...s) => console.log(s.map((v, i) => (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
// inj/measure.mjs — the same 16 inputs are run through every call site: at each step, // was the chain broken by a control, or did the precondition never hold. No randomness. import { STEP, LAYER, PRECOND, INPUT, classify, CALL, write } from "./chain.mjs"; const cost = (n) => STEP[n - 2]?.cost ?? 0; // items accumulated if broken at step n function run(input, deployed) { for (const a of STEP) { const k = LAYER.find((x) => x.breaks === a.n && deployed.includes(x.name) && x.cond(input)); if (k) return { stop: a.n, reason: "control", cost: cost(a.n) }; if (PRECOND[a.n] && !PRECOND[a.n](input)) return { stop: a.n, reason: "precond", cost: cost(a.n) }; } return { stop: 7, reason: "completed", cost: STEP[5].cost }; } console.log(`${STEP.length}-step chain, ${LAYER.length} layers, ` + `${CALL.length} call sites, ${INPUT.length} fictional inputs\n`); const A = [-17, 9, 25, 13, 12]; console.log("1. layer-to-step mapping"); write(A, "layer", "breaks", "cond", "legit cost", "call sites"); for (const k of LAYER) write(A, k.name, `step ${k.breaks}`, k.cond({ contract: true }) ? "unconditional" : "depends on input class", k.legit, CALL.filter(([, d]) => d.includes(k.name)).length); const B = [-29, 8, 14, 14, 13]; console.log("\n2. chain per call site"); write(B, "call site", "layers", "first break", "uncontrolled", "completing"); let uncontrolled = 0; for (const [name, d] of CALL) { const broken = STEP.filter((a) => LAYER.some((x) => x.breaks === a.n && d.includes(x.name))); const unconditional = broken.filter((a) => LAYER.find((x) => x.breaks === a.n).cond({ contract: true })); const done = INPUT.filter((g) => run(g, d).stop === 7).length; uncontrolled += STEP.length - broken.length; write(B, name, d.length, unconditional.length ? `step ${unconditional[0].n}` : "none", STEP.length - broken.length, `${done}/${INPUT.length}`); } console.log(`no control exists in ${uncontrolled} of the ${CALL.length * STEP.length} step-site combinations`); const PAPER = ["separate-operand", "context-escaping", "plain-response"]; console.log(`sites where the paper design (${PAPER.join(" + ")}) is complete: ` + `${CALL.filter(([, d]) => PAPER.every((k) => d.includes(k))).length}/${CALL.length}`); const C = [-20, 7, 15, 15, 13, 12]; console.log("\n3. stops per input class (16 inputs x 12 sites)"); write(C, "input class", "inputs", "control stop", "precond stop", "completed", "cost"); for (const s of [...new Set(INPUT.map(classify))]) { const set = INPUT.filter((g) => classify(g) === s); const r = set.flatMap((g) => CALL.map(([, d]) => run(g, d))); write(C, s, set.length, r.filter((x) => x.reason === "control").length, r.filter((x) => x.reason === "precond").length, r.filter((x) => x.reason === "completed").length, r.reduce((t, x) => t + x.cost, 0)); } const all = INPUT.flatMap((g) => CALL.map(([name, d]) => ({ name, ...run(g, d) }))); const complete = all.filter((x) => x.reason === "completed"); console.log(`${complete.length} of ${all.length} combinations complete all six steps, ` + `all at these two sites: ${[...new Set(complete.map((x) => x.name))].join(", ")}`); const D = [-14, 17, 18, 13]; console.log("\n4. the cost of breaking late"); write(D, "step broken", "items accumulated", "breaking layer", "legit cost"); for (const a of STEP) { const k = LAYER.find((x) => x.breaks === a.n); write(D, `step ${a.n}`, cost(a.n), k.name, k.legit); } console.log(`${STEP[5].cost} items if never broken; the six layers' total legitimate-work cost is ` + `${LAYER.reduce((t, k) => t + k.legit, 0)} items/day, the two that break the first two steps total ` + `${LAYER.slice(0, 2).reduce((t, k) => t + k.legit, 0)}`);
6-step chain, 6 layers, 12 call sites, 16 fictional inputs 1. layer-to-step mapping layer breaks cond legit cost call sites field-contract step 1 depends on input class 3 7 separate-operand step 2 unconditional 0 9 context-escaping step 3 unconditional 1 4 least-privilege step 4 unconditional 6 4 plain-response step 5 unconditional 4 4 exit-limit step 6 unconditional 9 2 2. chain per call site call site layers first break uncontrolled completing subscriber-portal.search 4 step 2 2 0/16 subscriber-portal.billing 3 step 2 3 0/16 subscriber-portal.dispute 3 step 2 3 0/16 collector.read-write 2 step 2 4 0/16 collector.bulk-transfer 3 step 2 3 0/16 validator.threshold 2 step 2 4 0/16 validator.archive-search 1 none 5 2/16 billing-generation.period 3 step 2 3 0/16 billing-generation.report 2 step 3 4 0/16 work-order.create 3 step 2 3 0/16 work-order.field-note 0 none 6 4/16 branch-endpoint.query 4 step 2 2 0/16 no control exists in 42 of the 72 step-site combinations sites where the paper design (separate-operand + context-escaping + plain-response) is complete: 1/12 3. stops per input class (16 inputs x 12 sites) input class inputs control stop precond stop completed cost delimiter-bearing 4 40 4 4 2400 long-but-compliant 2 20 4 0 0 plain-compliant 2 20 4 0 0 off-contract 8 88 6 2 1200 6 of 192 combinations complete all six steps, all at these two sites: validator.archive-search, work-order.field-note 4. the cost of breaking late step broken items accumulated breaking layer legit cost step 1 0 field-contract 3 step 2 0 separate-operand 0 step 3 0 context-escaping 1 step 4 40 least-privilege 6 step 5 120 plain-response 4 step 6 300 exit-limit 9 480 items if never broken; the six layers' total legitimate-work cost is 23 items/day, the two that break the first two steps total 3
The Layer’s Condition
The first table separates the six layers by a single column: condition. Five layers are unconditional — wherever they are deployed, they break the same step for every input. One is conditional: the field contract only breaks the chain for input that violates the contract. For an input that complies with the contract and carries the delimiter class, the contract layer breaks no step at all.
This distinction is the point most often misread in practice. Contract validation is cited as an injection defense; what measurement shows is that it only stops inputs that are formally out of bounds. The delimiter character class inside a legitimate subscriber name still complies with that field’s contract.
The second distinction is in the cost column. The layer that breaks the second step has a legitimate-work cost of zero: passing the value as a separate operand rejects no legitimate request. The exit limit that breaks the sixth step has a cost of 9 items/day. The six layers’ total cost is 23, while the two that break the first two steps total 3. Breaking early does not just lose fewer items; breaking itself is also cheaper.
Breaking Per Call Site
The second table is this lesson’s real measurement. The class is one, the chain is one — but the defense is deployed per call site, and twelve sites produce twelve different outcomes.
At nine of the twelve sites, the chain breaks at the second step, and a single layer does it. At one site, the first break shifts to the third step. At two sites there is no unconditional layer at all: there, the chain only stops when the input’s preconditions fail to hold, not because of the defense. Of the 72 combinations formed by the twelve call sites and six steps, 42 have no control at all.
The distribution of uncontrolled combinations is not uniform either. Four layers are present at four or fewer of the twelve sites; the most common layer is at nine sites. The count shows that coverage must be measured per layer: a layer existing does not mean it exists everywhere, and the chain runs through its weakest call site.
The last line gives the difference between paper and the field. The previous lesson recorded the injection chain as having a “control at three steps”; that record assumes the three layers are present everywhere. The measurement says that design is complete at only 1 of the organization’s 12 call sites. No defense claim written at the system level counts as verified until it is counted per call site.
Input Class and Stop
The third table splits the 16 inputs into four classes and counts where the 192 input-site combinations stop. Two columns are kept separate: did a control stop the chain, or did the input’s precondition fail to hold.
Inputs that do not carry the delimiter class stop on their own at the third step — with no
defense doing any work. That is what makes the plain-compliant and long-but-compliant
classes’ cost column zero. A defense saying “no problem found” often just means the chain was
already without a precondition for that input.
The four inputs that do carry the delimiter class, on the other hand, are stopped by a control in 40 of 48 combinations, the precondition fails to hold in 4, and the chain completes in 4. Those same four inputs alone carry a cost of 2,400 items at the two sites with incomplete deployment.
Of the 192 combinations, only 6 complete all six steps, and all six are at the same two call sites. Those two sites carry a cost of 2,400 and 1,200 items. Risk does not depend on the item itself, but on how many sites it is uncontrolled at: the class is the same at all twelve sites, the count piles up at two.
The Cost of Breaking Late
The fourth table gives the cost step by step. A chain broken in the first three steps has zero cost: parsing never happened. If it breaks at the fourth step, 40 fictional items are affected; at the fifth, 120; at the sixth, 300; if never broken, 480.
The three zero-cost steps do not mean breaking at those steps is without payoff. Breaking there means an incident never happens at all; the only thing that lands in the record is a rejected request. Breaking after the fourth step, on the other hand, always produces an incident record, a review, and a scope question — that is the second cost the cost column does not write down.
The jump in cost is not even. The move from the third step to the fourth goes from 0 to 40; from the fifth to the sixth, from 120 to 300. Toward the end of the chain, each step is more expensive than the last, because each step multiplies what the previous one gained. Least privilege and the exit limit are “late” defenses for this reason: they work, but by the time they engage, part of the loss has already happened.
Summary
- The injection class is a six-step chain, and each step depends on a precondition: a reachable endpoint, a concatenating call site, a value carrying the delimiter class, broad privilege, a detailed return, unlimited exit.
- Five of the six layers break unconditionally, one is conditional: the field contract only breaks for input outside the contract, and breaks no step at all for input that complies with the contract while carrying the delimiter class.
- The layer that breaks the second step has a legitimate-work cost of 0, the one that breaks the sixth has 9 items/day; the six layers total 23, the two that break the first two steps total 3.
- At 9 of the twelve call sites the chain breaks at the second step, at 1 at the third, at 2 there is no unconditional layer at all. 42 of the 72 step-site combinations have no control, and the three-layer paper design is complete at only 1 of the 12 sites.
- 6 of the 192 input-site combinations complete all six steps, and all six are at the same two call sites; from the fourth step on, the cost is 40, 120, 300 items, and 480 if never broken.
Next Step
In this lesson’s chain, the value was written into an interpreter’s text, and the interpreter was inside the server. The same step sequence takes a different shape when the interpreter is in the user’s browser: the value passes through the server, lands on a page, and parsing happens on another machine. The next lesson measures that shift — why the three separate forms have different step counts, how far the number of steps broken by server-side layers falls when a form never routes the value through the server at all, and what it means for the measure to shift from breaking to detection in that case.
To keep your progress and take notes, Log in
My notes
Log in to take notes.