AP-0104Identity & accessSeverity · High

Object ownership missing on a generated endpoint

An endpoint accepts an object identifier from the caller and returns or changes the object without confirming the caller owns it. It is the most common finding in code written by assistants, which copy the shape of neighbouring handlers but not their authorisation.

At a glance

Controls
OWASP API1:2023 · SOC 2 CC6.1 · ISO 27001 A.9.4
LikelihoodVery common in AI-written CRUD handlers
Effort to exploitLow: change one identifier in a request
Blast radiusEvery record the endpoint can reach
What review catchesLittle: the code reads correctly

Almost every product is a set of objects that belong to someone: orders, invoices, messages, files, projects. The rule that a person may touch only their own objects is so obvious that it is rarely written down, and code that is never told to check ownership does not check it. The endpoint fetches the object by its identifier, returns it, and looks completely correct while doing so.

Why it happens

A coding assistant asked for a handler that returns an order by its identifier writes exactly that. It reads the identifier from the path, queries the row, and sends it back. The prompt described the happy path, the model produced the happy path, and the ownership check was never part of the request. The generated code matches the style of the handlers around it, so a reviewer sees a familiar shape and reads past it.

The gap widens with velocity. When a team ships one handler a quarter, a human eventually notices the missing check. When an assistant ships a dozen a week, each new route is another chance for the same omission, and the review that used to catch it is now the bottleneck everyone is trying to route around.

How the attack unfolds

An attacker signs up for an ordinary account and watches the requests the product makes on their behalf. Any request carrying an identifier is a candidate: an order number, a user id, a file key, a sequential integer or a guessable string. They replay the request with a different identifier and read the response. If the object comes back, the endpoint is trusting the identifier alone, and the same weakness usually extends from reads to writes: update, export, cancel, refund, delete.

The endpoint was not broken into. It answered exactly the question it was asked, for an object that was never the caller’s to see.MATT research team

Why review misses it

Review reads for intent and for visible mistakes. A missing authorisation check is neither: nothing on the screen is wrong, and the intent looks satisfied because the object is returned. Static analysis has the same blind spot, because the code is syntactically clean and only semantically wrong. The defect is an absence, and absences do not show up in a diff.

Where it hides

What a reviewer can look for in the code and in the behaviour, before anyone proves it.

  • A handler queries an object by an identifier taken from the path, query or body, with no clause tying the query to the current user or tenant.
  • Authorisation is checked at the route (is the caller signed in) but not at the object (does this object belong to them).
  • Object identifiers are sequential or otherwise guessable and appear directly in URLs.
  • The same object is reachable from more than one route, and only one of them enforces ownership.

A real instance

GET /api/orders/{id} returned another customer’s order with the full delivery address and the last four digits of the card. The handler had been generated three days earlier, the review approved it, and the tests passed. The refund endpoint trusted the same identifier, so the same change issued a refund on a stranger’s purchase.

Details altered to protect the customer. Pattern, timing and outcome are as found.

How MATT tests it

The proof runs against the deployed product, on every release, whoever wrote the code.

  1. 01Enumerate every endpoint that accepts an object identifier in the path, query, body or a header.
  2. 02Request each with a second account’s identifier and compare the responses byte for byte.
  3. 03Chain a successful read into the write paths that share the identifier: update, export, cancel, delete.
  4. 04Repeat after every release, because new handlers are the usual source of a fresh instance.

The evidence

Before the fix
  1. GET /api/orders/48213200 OKReturns another customer’s order: email, total, card last four
  2. POST /api/orders/48213/refund200 OKRefund issued on an order the caller does not own
After the fix
  1. GET /api/orders/48213403 ForbiddenOwnership enforced at the data layer for every order route
  2. POST /api/orders/48213/refund403 ForbiddenSame rule holds on the write path, verified on every release
The rule, in plain English

A user must never be able to read or act on another user’s object.

Executable check

For every endpoint accepting an object identifier, a request carrying a foreign identifier must return 403 or 404, on every release.

Every rule MATT holds →

What the fix looks like

  1. 01

    Enforce ownership once, at the data layer

    Scope the query itself to the current user or tenant, so every route that loads the object inherits the check and no future handler can forget it.

  2. 02

    Make identifiers unguessable

    Prefer opaque identifiers over sequential integers, so enumeration is harder even where a check is briefly missing.

  3. 03

    Turn it into a rule that runs

    Every endpoint that accepts an object identifier must return 403 or 404 for a foreign identifier, checked on every release.

A check that was never written is found by proving the deployed behaviour, fixed with the engineer who shipped it, and kept fixed by running the control on every change. What is ExploitOps? →

ExploitOps discovery call

Understand where your security loop breaks.

A 15-minute call with a MATT co-founder to understand your product and whether an ExploitOps Review is worth doing. If there is a real need, we scope one with the research team.