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.
- Choose what to test, set options, and run
- An identity allowed to see other users' data
- Each column's verdict counts
- Same data: this identity got the original user's record
- A request that changes data, skipped unless you include writes
Open it
Section titled “Open it”-
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.
-
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 -
Add at least one identity to test (see below), then press Run (⌘ ↩).
The toolbar
Section titled “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) |
| 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
Section titled “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).
- 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
Section titled “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.
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
Section titled “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.
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.
How the replays are sent
Section titled “How the replays are sent”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
Section titled “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 |
| 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
Section titled “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
Section titled “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
Section titled “Reading a cell”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.
- The selected cell
- The verdict and why
- The original user's identifiers, found here
- Identical bodies, with the identifiers highlighted
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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.
cloakctl authz --session login-journey.cloaksession \ --identity "User B=Authorization: Bearer $USER_B_TOKEN" \ --identity anonymous \ --fail-on vulnerable --format markdownAn --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
Section titled “From an AI assistant”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.
"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
Section titled “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.