[Blog](https://excessionlabs.ai/blog/) · 2 November 2026 · Updated 3 November 2026 · [Excession Labs](https://excessionlabs.ai/about/)

# API authorization testing: a reproducible local workflow

Source page: https://excessionlabs.ai/blog/api-authorization-testing/

We reproduced an object ownership flaw with eight real HTTP requests against a small local API. Bob could read Alice's synthetic note in vulnerable mode. Enabling the server's ownership check denied that request while preserving both users' legitimate access.

This is a scripted Python and curl experiment with downloadable source and responses. No Incursion by Excession Labs agent or model executed the test. We checked the installed CLI's version and help separately. A suggested Incursion review prompt appears below; we did not run it.

On this page: [Scope](https://excessionlabs.ai/blog/api-authorization-testing/#scope) · [Reproduce the run](https://excessionlabs.ai/blog/api-authorization-testing/#reproduce) · [Observed results](https://excessionlabs.ai/blog/api-authorization-testing/#results) · [Verify the finding](https://excessionlabs.ai/blog/api-authorization-testing/#finding) · [Fix and retest](https://excessionlabs.ai/blog/api-authorization-testing/#fix) · [Optional Incursion review](https://excessionlabs.ai/blog/api-authorization-testing/#excession) · [Limits](https://excessionlabs.ai/blog/api-authorization-testing/#limits)

## Define the ownership boundary and test scope

API authorization testing checks whether an authenticated identity may perform a particular action on a particular object. Our policy is deliberately small: Alice owns `alice-note`, Bob owns `bob-note`, and neither user may read the other's note. Knowing an object's ID grants no access.

This fixture demonstrates [OWASP API1:2023, Broken Object Level Authorization](https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/). OWASP recommends checking the caller's permission for the requested action and record wherever client input selects an object. A valid login alone does not establish that permission.

- **Target:** two short-lived HTTP servers bound to IPv4 loopback, `127.0.0.1`. The runner selects free ports.

- **Allowed requests:** eight GET requests in total, using only `/objects/alice-note` and `/objects/bob-note`.

- **Identities:** public demo bearer values `demo-alice-token` and `demo-bob-token`. They are fixed test strings with no value outside this fixture.

- **Data:** two synthetic notes held in memory. There are no customer records, external services or database writes.

The fixture is intentionally insecure in its vulnerable mode. It has no configurable bind address and always listens on loopback. For a staging engagement, agree the hosts, accounts, records, allowed methods and stopping conditions with the system owner before replacing this scope. The [Excession Labs Acceptable Use Policy](https://excessionlabs.ai/aup/) describes authorized use. This article gives no permission to test another system.

## Run the downloadable fixture

Download [fixture.py](https://excessionlabs.ai/blog/evidence/api-authorization-testing/fixture.py) and [verify.py](https://excessionlabs.ai/blog/evidence/api-authorization-testing/verify.py) into the same directory. You need Python 3.9 or later and curl; the Python code uses only the standard library. Inspect the source, then run:

```
python3 verify.py --output rerun
```

The output directory must be new. The runner starts vulnerable and fixed servers in sequence, issues four real curl requests to each, saves the responses and stops the servers. Each curl request bypasses proxies with `--noproxy '*'`, has a one-second connection timeout and a three-second total timeout, and does not follow redirects. No login, model call or paid API is required.

Our recorded command was `python3 verify.py --output recorded-run`. It exited successfully with `{"checks": 8, "passed": true, "failures": []}` plus the evidence path. That means the demonstration matched its expected outcomes, including the intentionally vulnerable response.

The run on 2 November 2026 used fixture and runner version 1.0.0, Python 3.14.3 and curl 8.7.1 on macOS ARM64. Exact runtime strings, UTC timestamps, commands and source SHA-256 hashes are in [results.json](https://excessionlabs.ai/blog/evidence/api-authorization-testing/recorded-run/results.json). A rerun will use different ports, timestamps and response dates.

## Establish positive and negative controls

We first confirmed each identity could read its own object, then sent Bob's credentials with Alice's object ID. The anonymous request checks that authentication still applies. We repeated all four requests against a fresh server with the ownership check enabled.

Observed HTTP status codes from eight local requests

| Request identity | Requested object | Policy expects | Vulnerable mode | Fixed mode |
| --- | --- | --- | --- | --- |
| Alice | Alice's note | 200 | 200 | 200 |
| Bob | Bob's note | 200 | 200 | 200 |
| Bob | Alice's note | 403 | 200: note disclosed | 403: forbidden |
| Anonymous | Alice's note | 401 | 401 | 401 |

Successful owner requests matter: a server that denies every request would also deny cross-account access, but would break the application. The anonymous 401 shows that the vulnerable server authenticates callers, so the failure is in the object permission decision that follows.

## Verify the response before writing a finding

In the recorded vulnerable run, Bob's cross-account request used port 32818. This is the exact executed command, reformatted across lines:

```
curl --noproxy '*' --silent --show-error --include \
  --http1.1 --connect-timeout 1 --max-time 3 --request GET \
  --header 'Authorization: Bearer demo-bob-token' \
  http://127.0.0.1:32818/objects/alice-note
```

The [recorded HTTP response](https://excessionlabs.ai/blog/evidence/api-authorization-testing/recorded-run/vulnerable-bob-reads-alice.http) contains these status and body values; headers are omitted from this excerpt:

```
HTTP/1.0 200 OK

{"id": "alice-note", "owner": "alice", "text": "Alice synthetic note"}
```

The server's source maps Bob's bearer value to Bob, the selected object belongs to Alice, and the response includes Alice's note. Together, those facts verify a cross-account read under this fixture's policy. A 200 status without the object body would be weaker evidence: some APIs return a generic success envelope, an empty object or a login page.

A useful finding records the affected method and path, authenticated identity, object owner, intended policy, expected denial, observed status and disclosed fields. Here the impact is disclosure of one synthetic note. This experiment does not establish a production severity rating or show unauthorized modification. The [Incursion introduction](https://excessionlabs.ai/blog/introducing-excession/) explains the product's finding workflow; the finding here comes from the captured HTTP responses.

## Enable the server-side check and retest

The same fixture source has two modes. In vulnerable mode, it returns an existing object after authentication. Fixed mode enables this check before sending the object:

```
if self.server.mode == "fixed" and obj["owner"] != identity:
    return self.reply(403, {"error": "forbidden"})
```

This is an executed configuration change that activates a real server-side ownership check. It is not a generated patch or a production deployment. The check is sufficient for this toy policy, where every note has exactly one owner; real systems must also account for tenant membership, sharing, role permissions and the requested action.

The runner restarted the server in fixed mode on port 32824 and repeated the same identity, method and object combinations. Bob's request for Alice's note returned [403 with `{"error": "forbidden"}`](https://excessionlabs.ai/blog/evidence/api-authorization-testing/recorded-run/fixed-bob-reads-alice.http). Both owners still received their own note with 200, and the anonymous request still returned 401. The runner compares response bodies as well as status codes, so a denial containing the protected note would fail its checks.

To inspect the servers manually in two separate terminals, use `python3 fixture.py --mode vulnerable --port 8765` and `python3 fixture.py --mode fixed --port 8766`. Adjust the example curl URL to the corresponding port. Stop each server with Ctrl-C. The published run used the runner and ephemeral ports.

## Inspect the evidence bundle

- [Machine-readable results](https://excessionlabs.ai/blog/evidence/api-authorization-testing/recorded-run/results.json): all eight cases, identities, expected and observed outcomes, response filenames and hashes.

- [Exact recorded curl commands](https://excessionlabs.ai/blog/evidence/api-authorization-testing/recorded-run/commands.sh): the ports refer to the finished run. Use the runner to create fresh servers.

- [SHA-256 manifest](https://excessionlabs.ai/blog/evidence/api-authorization-testing/SHA256SUMS): compare downloaded sources and evidence. This is an integrity check, not independent attestation.

- [Fixture instructions and limitations](https://excessionlabs.ai/blog/evidence/api-authorization-testing/README.txt), [server source](https://excessionlabs.ai/blog/evidence/api-authorization-testing/fixture.py) and [verification source](https://excessionlabs.ai/blog/evidence/api-authorization-testing/verify.py).

All captured bearer values and bodies are synthetic public demo data. The bundle includes the actual HTTP response text, including dates and server headers. No production credentials or customer data were involved.

## Optional Incursion review: suggested, not executed

On 2 November 2026 the installed binary returned `excession 0.1.4`. We also ran `excession exec --help`; both commands and their outputs are saved in [cli-check.json](https://excessionlabs.ai/blog/evidence/api-authorization-testing/cli-check.json). CLI 0.1.5 is current at this update, so its help text can differ from the saved output. We did not launch an agent task because agent scope enforcement and model spend were not independently verified for this experiment. No Incursion sandbox, tool approval or model behavior was exercised.

The [unexecuted suggested prompt](https://excessionlabs.ai/blog/evidence/api-authorization-testing/suggested-excession-prompt.txt) for reviewing this bundle with Incursion asks it to read only the supplied source and responses, cite case IDs and distinguish observation from inference. Its central instruction is:

Using only the recorded evidence, explain whether Bob's authenticated GET for Alice's note crossed the stated ownership boundary in vulnerable mode, and whether the identical identity and object were denied in fixed mode.

That suggestion is a review of existing evidence, not an independent retest. Prompt instructions do not enforce network scope or containment. Consult the [current CLI documentation](https://excessionlabs.ai/docs/) and review your tool permissions before running an agent.

## What this experiment does not cover

Eight requests against one toy endpoint demonstrate one read-access boundary. We did not test writes, deletion, lists, alternate encodings, GraphQL, nested resources, shared objects, tenant hierarchies, caches, JWT validation or production infrastructure. The fixed fixture's result cannot establish that an unrelated API is secure.

For a real engagement, turn the application's permission policy into a test matrix, retain both successful and denied controls, and inspect returned data. Keep failed or inconclusive runs alongside successful reproductions. Extend the matrix only within the agreed scope, and retest the same cases after each authorization change.
