---
title: 'Request Forgery'
source: 'https://academia.sh/en/courses/attack-techniques/request-forgery'
course: 'Attack Techniques and the Threat Landscape'
language: en
updated: '2026-09-26T03:02:49+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "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"
---

# 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.

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.

```js
// 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(""));
```

```js
// 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.
