# Authorization matrix

> Replay every captured request as another user, an admin, or no credentials, and see cell by cell where one user gets another's data back — a broken object-level authorization check (OWASP API

Source: https://docs.subcloak.com/cloak-proxy/testing/authorization-matrix/

Added in Cloak Proxy 0.4.0.

Capture a session while signed in as one user. Add other identities — another user's token or cookies, an admin, or no credentials at all — and Cloak Proxy replays every captured request as each one. The result is a table: one row per endpoint, one column per identity, and in each cell a verdict and how similar the response was to the original user's. A request that returns the first user's data to a second user stands out in red.

That bug — a user reading or changing another user's object by its ID — is **broken object-level authorization**, or an IDOR. It is the [number-one risk in the OWASP API Security Top 10](https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/), and it is the first thing a penetration test checks. In Burp you do this with an extension such as Autorize, which is free; Postman, Charles and Proxyman have nothing like it built in.

*[Screenshot: The Authorization Matrix window for ShopApp: a Scope picker, Options and a Run Again button at the top; identity chips for Original, User B (sub 99, bob@example.com), Admin (marked Allowed) and No credentials; and a row per endpoint. GET /v1/orders/{id} and GET /v1/users/{id} show red Same data cells for User B, the Admin column shows Allowed, /v1/invoices/{id} is an Error for User B, and POST /v1/orders is marked Changes data and skipped.. Each row is an endpoint, each column an identity. Red “Same data” cells are likely IDORs; Admin is allowed to see other users' data, so its matches show as Allowed.]*

> **Only test systems you're allowed to test:**
> The Authorization Matrix sends real requests to the real server as each identity. Run it only against an API you own or have written permission to test. Replaying another user's credentials against a third party is not yours to do.

## Open it

1. Capture a session while signed in as one user (the "original" user). A normal browsing or app session is enough — the more endpoints it touches, the more the matrix covers.

2. Open the window one of these ways:

   | Where | Scope it opens with |
   |---|---|
   | **Tools › Authorization Matrix…** (<kbd>⌥</kbd> <kbd>⌘</kbd> <kbd>A</kbd>) | The selected requests, else the selected app, else everything |
   | Right-click a selection in the traffic list → **Test Authorization…** | Those requests |
   | Right-click an app or host in the sidebar → **Test Authorization…** | That app or host |

3. Add at least one identity to test (see below), then press **Run** (<kbd>⌘</kbd> <kbd>↩</kbd>).

> **On Linux:**
> The window opens with <kbd>Ctrl</kbd> <kbd>Alt</kbd> <kbd>A</kbd> (with the same scope), and from the same right-click menus. **Run** is <kbd>Ctrl</kbd> <kbd>Enter</kbd>; <kbd>Esc</kbd> cancels a run or closes the detail pane. Exports go through the desktop's Save dialog.

## The toolbar

| Control | What it does |
|---|---|
| Summary line | How many endpoints and identities the run covers, and whether it's read-only (for example `8 endpoints · 3 identities to test · read-only (1 write skipped)`) |
| **Scope** | What to test: the requests you opened it with, one app, one host, **The current filter** or **All captured traffic** |
| **Options** | Writes, 404 handling, one request per endpoint or all of them, self-signed certificates, how many at once and a requests-per-second cap (see [Options](#options)) |
| **Run** / **Run Again** | Replays every endpoint as each identity. While it runs, a progress bar shows "12 of 32 requests" with **Cancel** |
| Share | Export the finished matrix as Markdown, CSV or JSON |

## Identities

An identity is a name, a colour, and a set of credentials to substitute into each request before it is sent. Two are built in:

- **Original** — the request exactly as captured. This is the baseline every other identity is compared against. Cloak Proxy re-sends it too, because the captured session's tokens may have expired since (see [Expired baseline](#when-the-baseline-has-expired)).
- **No credentials** — the request with every credential stripped: `Authorization`, `Cookie`, common API-key headers (`X-API-Key`, `X-Auth-Token`…) and auth-looking query parameters (`?token=`, `?api_key=`). Detection is generic, so it works whatever the app uses. This column answers "can an anonymous caller read this?"

Add your own with **Add identity**. Each custom identity can set:

| Field | Example |
|---|---|
| Headers | `Authorization: Bearer …`, `X-API-Key: …`, any custom header |
| Cookies | `session`, merged into the `Cookie` header |
| Query parameters | `token`, `api_key` |
| Body fields | JSON fields like `access_token` |

When a custom identity runs, Cloak Proxy first strips every credential the captured request carried, then puts in this identity's own — so the replay carries exactly that identity and nothing of the original user.

### Admin and support roles

Some identities are supposed to see other users' data — an admin, a support agent, an internal service. Turn on **Allowed to see other users' data** in the identity editor (or right-click its chip) and that identity's matches show as a neutral **Allowed** instead of a red **Same data**. They are counted separately, don't add to the problem badges, and don't fail `cloakctl authz --fail-on vulnerable`. Its 401s, 403s and different-data cells are reported as usual.

*[Screenshot: The identity editor for Admin: a name, colour swatches with orange selected, a checked Allowed to see other users' data option with its explanation, a From a captured request menu, a Reveal values checkbox, a Headers section with a masked Authorization value and the line The token is a JWT for sub 1 · support@example.com, then empty Cookies, Query parameters and Body fields sections, and Cancel and Save buttons.. Adding an identity: a name, a colour, whether it may see other users' data, and the credentials to substitute. Token values are masked until you reveal them.]*

### From a captured request

The quickest way to add a second user is **From a captured request…** in the identity editor. Point it at a request made while signed in as that user (for example, capture a second session in a different browser profile), and Cloak Proxy copies its `Authorization` header, session cookies and auth query parameters into the identity, showing you what it took. No tokens to paste by hand.

### JWTs

When an identity's token is a JSON Web Token, the editor and the column header show its subject — `sub 99 · bob@example.com` — so you can tell at a glance which account each column is signed in as. This also feeds the verdict: the matrix learns the original user's identifiers (their `sub`, email or username, and the id in a captured `/me` or profile response) and flags any response that contains them.

> **Tokens are treated as secrets:**
> Credential values are masked in the editor and the detail pane, with a **Reveal values** checkbox. They are never written to logs, and exports mask the original user's identifiers.

## Scope

Choose what to test with **Scope** in the toolbar; the summary line under the title says what will run. Requests are grouped into **endpoints** by method and templated path — `GET /v1/users/42` and `GET /v1/users/77` become one `GET /v1/users/{id}` row, using the same path templating as the [endpoints catalog](https://docs.subcloak.com/cloak-proxy/insights/endpoints/). By default one representative request per endpoint is tested (the latest one that succeeded); turn on **Test every request, not one per endpoint** in **Options** to test them all.

These are always skipped: static assets (images, CSS, JS, fonts), `CONNECT` tunnels, WebSockets, and any request that never got a response.

## How the replays are sent

> **Replays bypass the proxy's tools and the traffic list:**
> Replays go **straight to the server**, the same way Repeat sends a request, but without passing through Cloak Proxy's debugging tools. Map Local, Rewrite and Breakpoints do **not** apply, so the results reflect what the real server does, not a local mock. Replays also don't appear in the main traffic list. Redirects to another host are never followed, so credentials stay on the original host.

Two limits in **Options** keep the load gentle: **At once** (how many replays run in parallel, default 4) and **Requests per second** (an optional cap across the whole run). **Cancel** stops everything immediately.

## Options

| Option | Default | What it does |
|---|---|---|
| **Include requests that change data** | Off | Also replay `POST`, `PUT`, `PATCH` and `DELETE`. Asks you to confirm first. See [Writes](#writes) |
| **Treat 404 as enforced** | On | Count a 404 as access denied when the original got a 2xx; some apps hide a forbidden object as "not found" rather than 403 |
| **Test every request, not one per endpoint** | Off | Replay every captured request instead of one representative per endpoint |
| **Accept self-signed certificates** | On | Don't check the server's certificate, for staging servers with their own |
| **Ignore Fields…** | None | JSONPath fields to leave out when comparing bodies, one per line (for example `$.meta.generatedAt`), on top of the built-in volatile fields |
| **At once** | 4 | How many replays run in parallel |
| **Requests per second** | No limit | A cap on how fast replays start, across the whole run |

## How verdicts are decided

Each cell compares the identity's response to the original user's response for that endpoint.

| Verdict | Meaning | When |
|---|---|---|
| **Enforced** | Access was correctly denied | 401 or 403; a redirect to a login page; or a 404 where the original got a 2xx (with the default option on) |
| **Same data** | A likely IDOR | A 2xx response whose body is very similar to the original's, or that contains the original user's own identifiers (their id, email or username) |
| **Different data** | Probably fine | A 2xx response that is clearly different — for example, this user got their own profile |
| **Allowed** | Expected | Same data, returned to an identity marked as allowed to see other users' data (an admin or support role) |
| **Error** | Couldn't tell from the server | A 5xx, a timeout or a connection error |
| **Inconclusive** | Nothing to compare | The original request itself didn't return data, so there's no baseline |
| **Skipped** | Not sent | A write method with writes turned off, or an endpoint you excluded |

Only **Same data** is tinted red, so the problems stand out. Under each column's name, the legend's icons show how many cells of each verdict that identity got (hover for the full counts). **Only problems** filters the table to the endpoints with a same-data cell, and the host menu narrows it to one host when the scope covers several. The **Original** column reads **Baseline** wherever the capture still works.

### Body similarity

For JSON, similarity compares the **shape** (the set of leaf paths) and the **values**, ignoring volatile fields that change every response — timestamps, request IDs, nonces, CSRF tokens and the like. You can drop more fields with **Options › Ignore Fields…**. For non-JSON bodies it falls back to a text similarity. The score (0–100%, shown as "96% alike") and the reasons ("the body is identical to the original"; "contains the original user's identifiers") show in the cell and the detail pane. A 2xx at 88% or more, or one containing the original user's identifiers, is **Same data**.

## Reading a cell

Click a cell to open the detail pane.

*[Screenshot: The detail pane for the GET /v1/users/{id} cell as User B: a Same data 200 verdict, 100% like the original, the reasons, the found identifiers 42 and ada@example.com, and a Diff tab that says Identical bodies above the Original and User B responses side by side, with 42 and ada@example.com highlighted in User B's response.. A selected cell: the verdict and why, the original user's identifiers found in the response, and the response diffed against the original.]*

Above the tabs, the verdict box gives the reasons and, under **Found**, the original user's identifiers that appeared in this response. It has three tabs:

- **Diff** — the response next to the original user's, one line per row with no wrapping (scroll sideways for long lines), changed lines tinted, and the original user's identifiers highlighted in this identity's response. A line above says **Identical bodies** or how many lines differ.
- **Request** — the request as it was actually sent, with the substituted credential headers highlighted.
- **Response** — the full status, headers and body this identity got back.

## Writes

Read-only methods (`GET`, `HEAD`, `OPTIONS`) run by default. Writes (`POST`, `PUT`, `PATCH`, `DELETE`) change data on the server, so they are **Skipped** until you turn on **Options › Include requests that change data**, which asks you to confirm first. Their rows carry a **Changes data** pill. To leave out one endpoint, right-click it and choose **Exclude from Runs**; it shows an **Excluded** pill and isn't sent in the next run (choose **Include in Runs** to undo).

## When the baseline has expired

The captured session's tokens expire. If the Original identity's re-run now returns 401 or 403, the whole run would otherwise read as "everything is enforced" when the real cause is a stale token. Cloak Proxy detects this and shows a banner instead: **The captured session's credentials have expired.** Recapture the session and run again.

## Exports

The export menu writes the finished run as:

- **Markdown** — a summary with the vulnerable endpoints first, then the full matrix. Good for a bug report or a pull request.
- **CSV** — one row per cell, for a spreadsheet.
- **JSON** — the result as data, for a pipeline.

The original user's identifiers are masked in every export.

## From the command line

Run the matrix in CI against a saved session with `cloakctl authz`. It exits non-zero with `--fail-on vulnerable` when any identity gets the original user's data back, so a build fails when a new IDOR appears.

```bash title="Terminal"
cloakctl authz --session login-journey.cloaksession \
  --identity "User B=Authorization: Bearer $USER_B_TOKEN" \
  --identity anonymous \
  --fail-on vulnerable --format markdown
```

An `--identity` is `Name=Header: value`, `Name=Cookie: session=…`, `Name=?token=…` (joined with `;` for several), or `anonymous`. Add `;allowed` for an admin or support role, for example `"Admin=Authorization: Bearer $ADMIN_TOKEN;allowed"`: its matches are reported as allowed and don't fail the run. Add `--include-writes`, `--every-request`, `--ignore-field JSONPATH` (repeatable), `--host HOST`, `--app NAME`, `--format markdown|json|csv` or `--out FILE` as needed. The No credentials column is included unless you pass `--no-anonymous`.

| Exit code | Meaning |
|---|---|
| `0` | The run finished, and no identity got the original user's data (or `--fail-on` wasn't given) |
| `1` | An error, such as an unreadable session or `--identity` |
| `2` | With `--fail-on vulnerable`: at least one Same data cell |

## From an AI assistant

The [MCP server](https://docs.subcloak.com/cloak-proxy/automation/mcp-server/) exposes the matrix as the `authorization_matrix` tool: pass the identities (each optionally `"allowed": true`) and a scope, and it returns the summary and the problem cells (or the Markdown report). It is read-only by default and bypasses the proxy's tools and the traffic list, like the window.

```bash title="Ask your assistant"
"Run the authorization matrix over ShopApp's traffic as a user with this token
 and as no credentials, and list any endpoints that leak the original user's data."
```

## Limits and safety

- The matrix tests what was captured. An endpoint the original session never called isn't in the table.
- "Same data" is a strong signal, not proof. Read the cell: an endpoint that returns the same public data to everyone (a product catalogue) is similar by design and isn't a leak. The matched-identifier reasons tell the two apart.
- Replays reach the real server. Keep writes off unless you mean to send them, and only test systems you're allowed to test.
- Identities are saved per workspace through a store that test and gallery runs redirect, so a self-test never reads or writes your real identities.

## Related

  - [Security insights](https://docs.subcloak.com/cloak-proxy/insights/security/): Per-host grades, leaked secrets and security-header issues.
  - [Repeat and compose](https://docs.subcloak.com/cloak-proxy/tools/repeat-and-compose/): Resend and edit a single request against the server.
  - [Compare requests](https://docs.subcloak.com/cloak-proxy/inspect/compare-requests/): Diff two requests and responses side by side.
  - [MCP server](https://docs.subcloak.com/cloak-proxy/automation/mcp-server/): Drive Cloak Proxy, including the matrix, from an AI assistant.
