Lesson 17 / 22
Request Forgery
Measuring the two directions of request forgery as separate chains: in the browser direction, the five steps' preconditions are split between the victim, the browser, and the server; in the server direction, forty percent of the cost is realized at the third step; two of eleven endpoints cannot be broken at all; removing a single layer produces a forty-five-transaction difference; and the two directions' costs are measured in separate units.
Published Updated
In this lesson
- Models the two directions of request forgery as separate step sequences and records each step's precondition owner
- Measures the first broken link and the realized cost per endpoint in the same run
- Compares the two directions' cost curves and shows in which direction breaking late does not zero out the cost
Contents
In the previous lesson, code ran in the victim’s browser. There is a quieter thing the same browser can do without running any code at all: send a request with the victim’s identity. Request forgery is that class, and it has two directions.
In the first direction, what is forged is the victim’s identity: the request leaves the victim’s browser and automatically carries the session item along with it. In the second direction, what is forged is the server’s network position: the request leaves the server and reaches an internal endpoint that cannot be accessed from outside. The two directions share the same class name because both involve sending a request with someone else’s authority; but their chains, precondition owners, and cost units are separate.
Two Chains, Two Precondition Owners
Browser direction, five steps. (1) The victim has an open session on the target — precondition: the session item sits in the browser and has not expired. (2) The victim reaches a page chosen by the attacker — precondition: a delivery channel exists. (3) A request leaves that page for the target, and the session item is automatically attached to it — precondition: the same-site policy does not exclude this request. (4) The endpoint accepts the request — precondition: the endpoint does not require an item that proves the request’s origin. (5) The transaction becomes permanent — precondition: the transaction completes in a single step.
Server direction, five steps. (1) An endpoint sends a request to an address given from outside — precondition: the target address comes from a request field. (2) Address resolution lands on an address inside the internal network — precondition: there is no check after resolution. (3) The request reaches the internal endpoint — precondition: the internal endpoint is reachable at the network layer. (4) The internal endpoint answers the request without asking for identity — precondition: internal endpoints trust based on location. (5) The response returns to the caller — precondition: the response body or an error difference is reflected back outward.
The first chain’s first two steps have their precondition in the victim’s hands; there is no foothold for the defense there. In the second chain, no step’s precondition belongs to the victim — two are in the defender’s own infrastructure, two in the network, one in an internal service. This explains why the two directions’ defense responsibility falls to different places.
TA14: the two directions are separate chains; owner is the party that holds the step’s
precondition. TA15: each layer breaks a single step in a single direction. TA16: the
unit of cost changes by direction, and the two units are not added together. TA17:
curve[i] gives how much of the cost is realized once step i is complete.
// rf/direction.mjs — request forgery's two directions, step sequences, and layers (model). // No real endpoint, no real address, and no request body at all. export const DIRECTION = { browser: { steps: ["open-session", "delivery", "auto-attach", "endpoint-acceptance", "persistence"], owner: ["victim", "victim", "browser", "server", "server"], unit: "transaction", curve: [0, 0, 0, 0, 1], }, server: { steps: ["address-from-outside", "resolution", "reach-internal-endpoint", "unauthenticated-response", "reflect-back"], owner: ["server", "network", "network", "internal-service", "server"], unit: "internal endpoint", curve: [0, 0, 0.4, 0.8, 1], }, }; export const LAYER = [ { name: "same-site-cookie", direction: "browser", breaks: 3, legit: 2 }, { name: "request-token", direction: "browser", breaks: 4, legit: 5 }, { name: "origin-check", direction: "browser", breaks: 4, legit: 3 }, { name: "re-authentication", direction: "browser", breaks: 5, legit: 11 }, { name: "egress-allowlist", direction: "server", breaks: 1, legit: 4 }, { name: "resolution-check", direction: "server", breaks: 2, legit: 1 }, { name: "network-segmentation", direction: "server", breaks: 3, legit: 8 }, { name: "internal-endpoint-identity", direction: "server", breaks: 4, legit: 6 }, { name: "plain-response", direction: "server", breaks: 5, legit: 2 }, ]; // The fictional measurement network's eleven endpoints: direction, deployed layers, and cost. export const ENDPOINT = [ ["subscriber-portal.plan-change", "browser", ["same-site-cookie", "request-token"], 60], ["subscriber-portal.address-update", "browser", ["same-site-cookie"], 45], ["subscriber-portal.dispute-open", "browser", ["request-token", "origin-check"], 30], ["field-app.work-order-close", "browser", ["same-site-cookie", "re-authentication"], 120], ["admin.threshold-setting", "browser", [], 8], ["admin.user-permission", "browser", ["request-token", "re-authentication"], 4], ["billing-generation.template-fetch", "server", ["egress-allowlist", "resolution-check"], 9], ["collector.feedback-endpoint", "server", ["resolution-check", "network-segmentation"], 14], ["work-order.map-preview", "server", ["plain-response"], 14], ["report.external-source-fetch", "server", [], 17], ["branch-endpoint.connection-test", "server", ["network-segmentation", "internal-endpoint-identity", "plain-response"], 6], ]; 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(""));
// rf/measure.mjs — the two directions are run across eleven endpoints: the first broken // link, the uncontrolled step count, and the direction-specific unit cost of breaking late. No randomness. import { DIRECTION, LAYER, ENDPOINT, write } from "./direction.mjs"; const firstBreak = (y, d) => Math.min(...LAYER .filter((k) => k.direction === y && d.includes(k.name)).map((k) => k.breaks), Infinity); // share of the cost realized in a chain that breaks at step n const ratio = (y, n) => (n === Infinity ? 1 : DIRECTION[y].curve[n - 2] ?? 0); console.log(`2 directions, ${LAYER.length} layers, ${ENDPOINT.length} fictional endpoints\n`); console.log("1. the two directions' step sequences"); for (const [name, z] of Object.entries(DIRECTION)) { console.log(`${name} direction — ${z.steps.length} steps, cost unit "${z.unit}", ` + `${LAYER.filter((k) => k.direction === name).length} layers`); z.steps.forEach((a, i) => console.log(` ${i + 1}. ${a.padEnd(26)}precondition owner: ` + `${z.owner[i].padEnd(18)}cost realized on completion: ${(z.curve[i] * 100).toFixed(0)}%`)); } const B = [-37, -12, 8, 13, 14, 8, 14]; console.log("\n2. chain per endpoint"); write(B, "endpoint", "direction", "layers", "first break", "uncontrolled", "cost", "realized"); const status = ENDPOINT.map(([name, y, d, cost]) => { const h = firstBreak(y, d); const broken = new Set(LAYER.filter((k) => k.direction === y && d.includes(k.name)).map((k) => k.breaks)); return { name, y, d, cost, h, uncontrolled: DIRECTION[y].steps.length - broken.size, real: cost * ratio(y, h) }; }); for (const u of status) write(B, u.name, u.y, u.d.length, u.h === Infinity ? "not broken" : `step ${u.h}`, u.uncontrolled, u.cost, u.real.toFixed(1)); for (const y of Object.keys(DIRECTION)) { const k = status.filter((u) => u.y === y); console.log(`${y} direction: ${k.length} endpoints, total cost ` + `${k.reduce((t, u) => t + u.cost, 0)} ${DIRECTION[y].unit}s, realized ` + `${k.reduce((t, u) => t + u.real, 0).toFixed(1)}; endpoints unbroken ` + `${k.filter((u) => u.h === Infinity).length}`); } const C = [-29, -12, 12, 8, 17, 21]; console.log("\n3. if a layer is removed from every endpoint"); write(C, "layer", "direction", "deployment", "legit", "newly-unbroken", "realized difference"); for (const k of LAYER) { const set = status.filter((u) => u.y === k.direction); let newlyOpen = 0, diff = 0; for (const u of set) { const h2 = firstBreak(k.direction, u.d.filter((x) => x !== k.name)); if (u.h !== Infinity && h2 === Infinity) newlyOpen++; diff += u.cost * ratio(k.direction, h2) - u.real; } write(C, k.name, k.direction, set.filter((u) => u.d.includes(k.name)).length, k.legit, newlyOpen, `+${diff.toFixed(1)}`); } const D = [-8, 13, 13, 20, 19]; console.log("\n4. shape of the cost curve"); write(D, "step", "browser %", "server %", "browser endpoints", "server endpoints"); for (let n = 1; n <= 5; n++) write(D, `${n}.`, (DIRECTION.browser.curve[n - 1] * 100).toFixed(0), (DIRECTION.server.curve[n - 1] * 100).toFixed(0), status.filter((u) => u.y === "browser" && u.h === n).length, status.filter((u) => u.y === "server" && u.h === n).length); console.log(`in the browser direction, cost is realized only at the last step: breaking at ` + `the fourth step keeps the cost at zero`); console.log(`in the server direction, the third step produces 40% of the cost: breaking late does not zero out the cost`);
2 directions, 9 layers, 11 fictional endpoints 1. the two directions' step sequences browser direction — 5 steps, cost unit "transaction", 4 layers 1. open-session precondition owner: victim cost realized on completion: 0% 2. delivery precondition owner: victim cost realized on completion: 0% 3. auto-attach precondition owner: browser cost realized on completion: 0% 4. endpoint-acceptance precondition owner: server cost realized on completion: 0% 5. persistence precondition owner: server cost realized on completion: 100% server direction — 5 steps, cost unit "internal endpoint", 5 layers 1. address-from-outside precondition owner: server cost realized on completion: 0% 2. resolution precondition owner: network cost realized on completion: 0% 3. reach-internal-endpoint precondition owner: network cost realized on completion: 40% 4. unauthenticated-response precondition owner: internal-service cost realized on completion: 80% 5. reflect-back precondition owner: server cost realized on completion: 100% 2. chain per endpoint endpoint direction layers first break uncontrolled cost realized subscriber-portal.plan-change browser 2 step 3 3 60 0.0 subscriber-portal.address-update browser 1 step 3 4 45 0.0 subscriber-portal.dispute-open browser 2 step 4 4 30 0.0 field-app.work-order-close browser 2 step 3 3 120 0.0 admin.threshold-setting browser 0 not broken 5 8 8.0 admin.user-permission browser 2 step 4 3 4 0.0 billing-generation.template-fetch server 2 step 1 3 9 0.0 collector.feedback-endpoint server 2 step 2 3 14 0.0 work-order.map-preview server 1 step 5 4 14 11.2 report.external-source-fetch server 0 not broken 5 17 17.0 branch-endpoint.connection-test server 3 step 3 2 6 0.0 browser direction: 6 endpoints, total cost 267 transactions, realized 8.0; endpoints unbroken 1 server direction: 5 endpoints, total cost 60 internal endpoints, realized 28.2; endpoints unbroken 1 3. if a layer is removed from every endpoint layer direction deployment legit newly-unbroken realized difference same-site-cookie browser 3 2 1 +45.0 request-token browser 3 5 0 +0.0 origin-check browser 1 3 0 +0.0 re-authentication browser 2 11 0 +0.0 egress-allowlist server 1 4 0 +0.0 resolution-check server 2 1 0 +0.0 network-segmentation server 2 8 0 +2.4 internal-endpoint-identity server 1 6 0 +0.0 plain-response server 2 2 1 +2.8 4. shape of the cost curve step browser % server % browser endpoints server endpoints 1. 0 0 0 1 2. 0 0 0 1 3. 0 40 3 1 4. 0 80 2 0 5. 100 100 0 1 in the browser direction, cost is realized only at the last step: breaking at the fourth step keeps the cost at zero in the server direction, the third step produces 40% of the cost: breaking late does not zero out the cost
The Owner of the Precondition
The first output unpacks the two chains step by step and records who holds each step’s precondition. In the browser direction, two steps’ owner is the victim, one is the browser, two are the server. The number of steps the defender can decide directly on is two out of five; in the remaining three, they have only an indirect effect.
In the server direction, the table flips: two steps’ owner is the server itself, two are the network, one is an internal service. No step’s precondition belongs to the victim. This direction does not depend on a user mistake at all; it plays out entirely within the organization’s own configuration. This is why the defense responsibility for two chains gathered under the same class name falls to different teams.
Breaking Per Endpoint
The second output measures eleven endpoints. Of the six endpoints in the browser direction, three break at the third step, two at the fourth step; one never breaks. Of the five endpoints in the server direction, one breaks at the first step, one at the second, one at the third, one at the fifth; one never breaks.
The realized column makes the difference concrete. In the browser direction, the total cost
is 267 transactions, and the realized amount is only 8 — because on the five endpoints that
break, the chain stops before reaching the fifth step, and in the browser direction, cost only
accrues at the last step. In the server direction, the total cost is 60 internal endpoints, and
the realized amount is 28.2; the endpoint that cannot be broken produces all of its 17, and
the endpoint that breaks at the fifth step produces 11.2 of its 14.
The uncontrolled column gives a second reading. The endpoint with the most layers,
branch-endpoint.connection-test, breaks at the third step, and only two of its five steps are
uncontrolled; the two endpoints with the fewest layers have all five of their five steps
uncontrolled. On endpoints in between, the layer count and the uncontrolled-step count are not a
one-to-one complement, because two layers can break the same step:
subscriber-portal.dispute-open carries two layers, but both break the fourth step, so its
uncontrolled-step count is four. Two layers landing on the same step does not win a second
link.
The two totals can be written side by side but cannot be added together. One is daily transactions, the other is the number of internal endpoints reached. When a defense report says “two endpoints have no protection,” it is talking about two different magnitudes: one affects 8 fictional transactions a day, the other opens access to 17 internal endpoints.
Removing a Single Layer
The third output is a sensitivity measurement: each layer is removed from every endpoint, and the resulting difference in realized cost is recorded. Of nine layers, six have a difference of zero — when removed, the breaking point shifts backward, but the cost does not change, because another layer stands behind them.
Three layers do produce a difference. If same-site-cookie is removed, one endpoint becomes
unbreakable and the realized cost rises by 45 transactions; its legitimate-work cost is only 2.
plain-response produces a difference of 2.8, network-segmentation of 2.4. By contrast, when
re-authentication, with a legitimate-work cost of 11, is removed, the difference is zero: the
most expensive layer adds nothing in this deployment.
This shows that layer selection has to be read from the deployment, not from a list. A layer’s value is not in its own strength, but in whether another layer that breaks before it exists on the endpoints where it is deployed.
The reverse reading of the measurement comes from the same numbers. None of the six layers with a zero difference are unnecessary; they are only redundant in this deployment. The moment the layer in front of them is removed from an endpoint, they start producing a difference. The question of whether a layer can be removed cannot be asked in isolation, then: the answer depends on which steps the other layers standing on the same endpoint hold, and it has to be recalculated every time the deployment changes.
The Shape of the Cost Curve
The fourth output gives the two directions’ real difference. In the browser direction, the cost curve is zero for four steps and jumps to 100% all at once at the fifth step. Breaking at the fourth step and breaking at the first step give the same result: zero cost. In this direction, there is almost no such thing as “breaking late”; the chain either completes or it does not.
In the server direction, the curve is stepped: 40% of the cost is realized once the third step completes, 80% once the fourth completes. Breaking at the fifth step — that is, blocking the response from being reflected back out — prevents only 20% of the cost. The internal endpoints have already been reached, and which services are alive has already been learned; the response not coming back does not undo that.
The gain from breaking early is not the same in the two chains gathered under the same class name. In the browser direction, every point before the last link has equal value; in the server direction, every step is more expensive than the one before it, and the defense cannot afford to fall behind the second step.
Summary
- Request forgery’s two directions are separate chains: one uses the victim’s identity, the other the server’s network position. Both are five steps, but their precondition owners differ.
- In the browser direction, 2 of the five steps’ preconditions are in the victim’s hands; in the server direction, no step’s precondition belongs to the victim, and the entire chain plays out within the organization’s own configuration.
- In 2 of the eleven endpoints, no link breaks at all: one is 8 fictional transactions a day, the other 17 internal endpoints. The two numbers are in separate units and are not added together.
- Removing 6 of the nine layers does not change the realized cost; removing a layer with a legitimate-work cost of 2 produces a 45-transaction difference, while removing the most expensive layer (legitimate-work cost 11) produces 0.
- The cost curves differ: in the browser direction, cost is realized only at the fifth step; in the server direction, 40% is realized by the third step and 80% by the fourth. Breaking late leaves a real loss only in the second direction.
Next Step
In this lesson’s server direction, the request went to an address, and the address coming from outside was the chain’s first link. The same structure turns into a different class when, instead of an address, it is a file path: the endpoint takes an item name, appends it to a root directory, and reads the result. The next lesson counts that chain — where the path component comes from, which check remains after the concatenation, how the cost is determined by the number of items the process identity can read, and at which step an indirect identifier mapping breaks the chain.
To keep your progress and take notes, Log in
My notes
Log in to take notes.