---
title: 'Cross-Site Scripting'
source: 'https://academia.sh/en/courses/attack-techniques/cross-site-scripting'
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 three forms of cross-site scripting as separate step sequences and compares their step counts'
    - 'Counts how many steps the server-side layers break in the DOM-based form'
    - 'Measures the first broken link per page and gives the cost of breaking late in sessions'
---

# Cross-Site Scripting

Measuring the three forms as separate step sequences: reflected five, stored six, DOM-based four steps; the number of steps the three server-side layers break in the DOM-based form being zero, no link breaking in three of the twenty-four combinations across eight pages, the daily cost of one hundred ninety sessions for the seven combinations broken only at the last step, and a single browser-side layer closing off all three forms at once.

In the previous lesson's chain, the interpreter was inside the server, and three of the six
steps passed through the server. This lesson measures the same step sequence when the
interpreter is **in the user's browser**. The only thing that changes is where parsing
happens; the result is that the chain's length and the breakable link land in three separate
places across three separate forms.

The three forms are separated: the **reflected** form, where the value comes back in the same
response; the **stored** form, where the value is first written to storage and then served to
every viewer; and the **DOM-based** form, where the value never touches the server at all.
Throughout the measurement, no script body, no value, and no page source is ever written out;
the forms are referred to only by their step sequences.

## Three Step Sequences

**Reflected form, five steps.** (1) The value reaches the endpoint in a request field —
precondition: the endpoint writes that field back into its response. (2) The value lands in a
position that will be read as code in the response — precondition: no encoding is applied for
that position's context. (3) The link reaches the victim and the victim opens it with their
own session — precondition: a delivery channel exists. (4) The browser executes the placed
value — precondition: the page's content policy allows this source. (5) The running code
accesses the session item and carries it out — precondition: the cookie is script-accessible.

**Stored form, six steps.** The reflected form's first step splits in two: the value first
enters **storage** through a write endpoint, then lands in the response along a view path. In
exchange, the third step's precondition **disappears**: nobody needs to be directed to a link,
anyone who opens the page for their own work completes the chain themselves. The chain gets
one step longer and the delivery link comes free.

**DOM-based form, four steps.** (1) The value is read from a client-side source — precondition:
a source that never reaches the server is used, such as the URL fragment or local storage.
(2) The client script writes the value to a target that is interpreted as code — precondition:
the target selection is not fixed. (3) The browser executes it. (4) Access and exit. This
sequence has **no step that passes through the server**; the server never sees the request, it
never lands in its log, and it never alters its response.

What the three sequences share is the last two steps: execution and exit. That is why the
defense can stand in two separate places — one blocks the value from landing in the response,
the other blocks the browser from executing it. Where they diverge is the start: the reflected
and stored forms pass through the server, the third does not.

**TA10:** each form is a separate step sequence. **TA11:** each layer breaks a specific step in
each form; in a form it does not break, it has no value. **TA12:** for per-link delivery the
cost is a fictional 12 people, for per-view delivery it is the page's daily view count.
**TA13:** the first broken link is the smallest step index broken by the layers deployed on
that page.

```js
// xss/form.mjs — cross-site scripting's three forms, their step sequences, and layers
// (model). There is no real page, no real value, and no script body at all.
export const FORM = {
  reflected: { steps: ["entry", "placement", "delivery", "execution", "exit"],
    side: ["server", "server", "victim", "browser", "browser"], delivery: "per link" },
  stored: { steps: ["write", "storage", "placement", "view", "execution", "exit"],
    side: ["server", "server", "server", "victim", "browser", "browser"],
    delivery: "per view" },
  domBased: { steps: ["client-source", "write-to-target", "execution", "exit"],
    side: ["browser", "browser", "browser", "browser"], delivery: "per link" },
};

// breaks[form] = 1-based step index, or 0 (does not break any step in that form).
export const LAYER = [
  { name: "input-contract", side: "server",
    breaks: { reflected: 1, stored: 1, domBased: 0 }, legit: 3 },
  { name: "storage-sanitization", side: "server",
    breaks: { reflected: 0, stored: 2, domBased: 0 }, legit: 2 },
  { name: "output-encoding", side: "server",
    breaks: { reflected: 2, stored: 3, domBased: 0 }, legit: 0 },
  { name: "content-policy", side: "browser",
    breaks: { reflected: 4, stored: 5, domBased: 3 }, legit: 7 },
  { name: "client-target-selection", side: "client-code",
    breaks: { reflected: 0, stored: 0, domBased: 2 }, legit: 1 },
  { name: "cookie-flag", side: "browser",
    breaks: { reflected: 5, stored: 6, domBased: 4 }, legit: 4 },
];

export const LINK = 12;

// The fictional measurement network's eight pages: deployed layers and daily view count.
export const PAGE = [
  ["subscriber-portal.search", ["input-contract", "output-encoding", "content-policy", "cookie-flag"], 210],
  ["subscriber-portal.dispute", ["input-contract", "storage-sanitization", "output-encoding", "cookie-flag"], 90],
  ["subscriber-portal.billing", ["output-encoding", "content-policy", "cookie-flag"], 340],
  ["field-app.work-order", ["input-contract", "output-encoding", "cookie-flag"], 160],
  ["field-app.field-note", ["cookie-flag"], 75],
  ["branch-endpoint.dashboard", ["output-encoding", "content-policy"], 40],
  ["branch-endpoint.report-link", ["client-target-selection", "cookie-flag"], 55],
  ["admin.metric-search", [], 20],
];

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
// xss/measure.mjs — the three forms are run across eight pages: the first broken link,
// how many steps the server side breaks, and the sessions affected if unbroken. No randomness.
import { FORM, LAYER, PAGE, LINK, write } from "./form.mjs";

const forms = Object.keys(FORM);
const LABEL = { reflected: "reflected", stored: "stored", domBased: "DOM-based" };
const firstBreak = (b, d) => Math.min(...LAYER
  .filter((k) => d.includes(k.name) && k.breaks[b] > 0).map((k) => k.breaks[b]), Infinity);
const cost = (b, views) => (b === "stored" ? views : LINK);

console.log(`${forms.length} forms, ${LAYER.length} layers, ${PAGE.length} fictional pages\n`);

const A = [-16, 7, 11, 12, 20];
console.log("1. chain per form");
write(A, "form", "steps", "on server", "breakable", "delivery");
for (const b of forms)
  write(A, LABEL[b], FORM[b].steps.length, FORM[b].side.filter((t) => t === "server").length,
    LAYER.filter((k) => k.breaks[b] > 0).length, FORM[b].delivery);

const B = [-25, -14, 10, 8, 15];
console.log("\n2. layer-to-step mapping");
write(B, "layer", "side", "reflected", "stored", "DOM-based");
for (const k of LAYER)
  write(B, k.name, k.side, k.breaks.reflected || "-", k.breaks.stored || "-", k.breaks.domBased || "-");
const server = LAYER.filter((k) => k.side === "server");
console.log(`the ${server.length} server-side layers break this many steps in the DOM-based form: ` +
  `${server.filter((k) => k.breaks.domBased > 0).length}`);

const C = [-29, 7, 12, 12, 15];
console.log("\n3. first broken link per page");
write(C, "page", "layers", "reflected", "stored", "DOM-based");
let open = 0, sessions = 0;
for (const [name, d, views] of PAGE) {
  const h = forms.map((b) => firstBreak(b, d));
  h.forEach((v, i) => { if (v === Infinity) { open++; sessions += cost(forms[i], views); } });
  write(C, name, d.length, ...h.map((v) => (v === Infinity ? "not broken" : `step ${v}`)));
}
console.log(`no link breaks in ${open} of the ${PAGE.length * forms.length} page-form combinations; ` +
  `those combinations' daily cost is ${sessions} fictional sessions`);
const lateBreaks = PAGE.flatMap(([, d, views]) => forms.filter((b) =>
  firstBreak(b, d) === FORM[b].steps.length).map((b) => cost(b, views)));
console.log(`combinations broken only at the last step: ${lateBreaks.length}, daily cost ` +
  `${lateBreaks.reduce((a, x) => a + x, 0)} fictional sessions`);

const D = [-25, 10, 8, 15, 9, 20];
console.log("\n4. if a single layer covered every page");
write(D, "single layer", "reflected", "stored", "DOM-based", "legit", "remaining sessions");
for (const k of LAYER) {
  const remaining = PAGE.reduce((t, [, , views]) => t + forms.reduce((s, b) =>
    s + (k.breaks[b] > 0 ? 0 : cost(b, views)), 0), 0);
  write(D, k.name, k.breaks.reflected || "-", k.breaks.stored || "-", k.breaks.domBased || "-",
    k.legit, remaining);
}
const allOpen = PAGE.reduce((t, [, , views]) => t + forms.reduce((s, b) => s + cost(b, views), 0), 0);
console.log(`daily cost with no layer at all: ${allOpen} fictional sessions`);
```

```
3 forms, 6 layers, 8 fictional pages

1. chain per form
form              steps  on server   breakable            delivery
reflected             5          2           4            per link
stored                6          3           5            per view
DOM-based             4          0           3            per link

2. layer-to-step mapping
layer                    side           reflected  stored      DOM-based
input-contract           server                 1       1              -
storage-sanitization     server                 -       2              -
output-encoding          server                 2       3              -
content-policy           browser                4       5              3
client-target-selection  client-code            -       -              2
cookie-flag              browser                5       6              4
the 3 server-side layers break this many steps in the DOM-based form: 0

3. first broken link per page
page                          layers   reflected      stored      DOM-based
subscriber-portal.search           4      step 1      step 1         step 3
subscriber-portal.dispute          4      step 1      step 1         step 4
subscriber-portal.billing          3      step 2      step 3         step 3
field-app.work-order               3      step 1      step 1         step 4
field-app.field-note               1      step 5      step 6         step 4
branch-endpoint.dashboard          2      step 2      step 3         step 3
branch-endpoint.report-link        2      step 5      step 6         step 2
admin.metric-search                0  not broken  not broken     not broken
no link breaks in 3 of the 24 page-form combinations; those combinations' daily cost is 44 fictional sessions
combinations broken only at the last step: 7, daily cost 190 fictional sessions

4. if a single layer covered every page
single layer              reflected  stored      DOM-based    legit  remaining sessions
input-contract                    1       1              -        3                  96
storage-sanitization              -       2              -        2                 192
output-encoding                   2       3              -        0                  96
content-policy                    4       5              3        7                   0
client-target-selection           -       -              2        1                1086
cookie-flag                       5       6              4        4                   0
daily cost with no layer at all: 1182 fictional sessions
```

## The Chain the Server Never Sees

The last line of the second table is this lesson's main finding: the number of steps the three
server-side layers break in the DOM-based form is **zero**. The input contract never sees a
request, storage sanitization never sees a write, output encoding never sees a value written
into the response. The value never touches the server at all.

This has a second consequence, heavier than the first: the server **records none of this
chain's steps**. When a chain cannot be broken, the measure shifts to detection; here, detection
cannot happen server-side either, because there is no log line to look at. The only places this
form can be seen are a policy-violation report that describes browser behavior and the client
code's own logging.

The same line also highlights a layer class: `client-target-selection` only carries value in
this form, and it alone breaks the **second step**. No decision about hardening the server
affects this step.

This also determines where the measure moves. A defense report might count the three
server-side layers and write "three-layer protection," and that is true for two forms. For the
third form, the count is three, but the number of steps it breaks is zero — and the difference
stays invisible unless the report separates the two. This line is the reason for counting
**broken steps** instead of counting layers.

## Chain Length and Delivery

The first table puts the three forms side by side: 5, 6, and 4 steps. The longest chain is not
the safest one. The stored form has six steps, and the extra step **favors the attacker**: the
fourth step requires no delivery effort, anyone who opens the page for their own work completes
that step.

The number of layers that can break it reads the same way: four layers can break the reflected
form, five the stored form, three the DOM-based form. The stored form is the one seen by the
most layers because most of its steps pass through the server; the one seen by the fewest
layers is the one that never passes through the server at all. **A chain's visibility is
measured not by its length, but by how many steps it spends on the side where the defense
sits.**

The delivery column says the unit of cost differs too. In the reflected and DOM-based forms,
the cost is per link — a fictional 12 people. In the stored form, the cost is per view and
ranges from 20 to 340 depending on the page. Same class, same page, different form: **the cost
of breaking late can run up to twenty-eight times as high.**

## Breaking Per Page

The third table measures deployment. In **3** of the 24 combinations formed by eight pages and
three forms, no link breaks at all; all three are concentrated on a single page. That page has
no layer deployed at all, and its daily cost is 44 fictional sessions.

The subtler result is in seven combinations: there, the chain does break, but **only at the
last step**. On a page that carries the cookie flag but has no other layer, code runs, the page
changes, requests go out — the only link that breaks is the access at the very end. These seven
combinations have a daily cost of 190 sessions, and they show up as "protected" in the defense
report.

The table's left column gives the reason for this result. On the two pages with four layers,
the chain breaks at the first step; on pages with three layers, at the second or third step; on
the page with a single layer, at the fifth and sixth step. In this set, the layer count and the
index of the broken link move together, but not one to one: the three-layer
`subscriber-portal.billing` breaks at the second step, and the two-layer
`branch-endpoint.dashboard` also breaks at the second step. What matters is not the count, but
**which steps** the layers fall on.

The `branch-endpoint.report-link` row shows the directional difference between two layers. In
the DOM-based form, the chain breaks at the **second step**, because client target selection
sits there; in the reflected and stored forms, it only breaks at the fifth and sixth step. Same
page, same two layers, three different links depending on the form.

## A Single Layer's Coverage

The fourth table assumes each layer alone, deployed on every page, and counts the remaining
daily cost. With no layer at all, the cost is 1,182 fictional sessions.

Two rows give zero: the content policy and the cookie flag break all three forms, because both
are **browser-side** and all three forms converge there. Their legitimate-work cost is 7 and 4 —
the most expensive of the six layers.

The storage-sanitization row gives the limit of a layer tied to a single form: it breaks the
stored form at the second step, does nothing in the other two forms, and leaves 192 sessions
remaining — not the highest of the six rows, but one of the two layers tied to only a single
form.

Output encoding's legitimate-work cost is **zero**, and it breaks the reflected and stored forms at
the second and third step; but it does nothing in the DOM-based form and leaves 96 sessions
remaining. The cheapest layer breaks early but never sees one of the three forms; the layer
that covers all three forms at once is the most expensive one. **Coverage and earliness do not
converge in the same layer** — that is the measured reason both layers are needed.

## Summary

- The three forms are three separate chains: reflected 5, stored 6, DOM-based 4 steps. The
  stored form's extra step favors the attacker, because it removes the delivery link's
  precondition.
- The number of steps the 3 server-side layers break in the DOM-based form is 0, and none of
  that chain's steps land in a server log; there, the measure shifts from breaking to
  detection, and detection can only happen browser-side.
- The unit of cost changes by form: 12 fictional people per link, 20 to 340 per view. On the
  same page, the cost of breaking late can run up to twenty-eight times as high depending on
  the form.
- Of the 24 combinations formed by eight pages and three forms, 3 have no link breaking at all
  (44 sessions/day), and 7 break only at the last step (190 sessions/day).
- Each of the two browser-side layers alone closes off all three forms and brings the remaining
  cost to 0; their legitimate-work cost is 7 and 4. Output encoding, with a cost of 0, breaks early but
  leaves 96 sessions remaining because it never sees one of the forms.

## Next Step

In all three of this lesson's forms, the code ran in the victim's browser and accessed the
session item at the last step. There is a quieter thing the same browser can do: send a request
**with the victim's identity**, without running any code at all. The next lesson measures that
class — the direction in which a request travels through the victim's browser and the
direction in which it travels with the server's own identity are written as separate chains,
how many steps each direction has, which layer breaks which step, and why the two directions'
late-break costs cannot be measured in the same unit.
