Skip to content

Test your API

Authorization matrix

Since 0.4.0 Requires Cloak Proxy 0.4.0 or later.

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

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

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.

  1. Choose what to test, set options, and run
  2. An identity allowed to see other users' data
  3. Each column's verdict counts
  4. Same data: this identity got the original user's record
  5. A request that changes data, skipped unless you include writes
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.
  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… (⌥ ⌘ A) 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 (⌘ ↩).

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)
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

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).
  • 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.

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.

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.

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.

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.

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.

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

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.

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

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.

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.

Click a cell to open the detail pane.

A selected cell: the verdict and why, the original user's identifiers found in the response, and the response diffed against the original.

  1. The selected cell
  2. The verdict and why
  3. The original user's identifiers, found here
  4. Identical bodies, with the identifiers highlighted
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.

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.

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

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.

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.

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.

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

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

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."
  • 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.