A login check establishes who called an API. An authorization review asks whether that caller may perform this action on this resource, in this account, at this moment, and receive these fields. Start with the business rule, trace every entry point to the resource, and inspect where the server gets the caller, ownership, role and state used in its decision. Include lists, exports, background jobs and direct object routes; they often expose different paths to the same data. Record evidence for both the suspected gap and any control that defeats it. If deployment, downstream policy or caller context is unknown, name that limit without discarding what the source establishes. OWASP distinguishes authentication from authorization and calls for permission validation on every request in its Authorization Cheat Sheet.
Write the rule before reading the handler
“Requires login” is not a complete policy. A useful rule names an actor, operation, object, relationship and permitted output. For example: “A billing viewer may read invoices owned by their current account, but only a billing manager may approve a refund; neither role may see the card processor's internal notes.” The rule may also depend on the invoice's state or a time-limited delegation. Write those conditions down before judging a 200, 403 or source-code branch.
OWASP's API risks split several common mistakes: API1:2023 concerns access to the particular object selected by a client; API5:2023 concerns access to a function the caller should not invoke; and API3:2023 concerns protected properties read or changed within an otherwise accessible object. A single operation can have more than one of these gaps. The categories help describe evidence; they do not replace the application's actual policy.
Work an illustrative invoice case
Illustrative application and records only; no live system was assessed. Harbor Ledger serves three account-scoped routes: an invoice list, GET /invoices/{id}, and POST /invoices/{id}/approve-refund. A signed-in user belongs to account Cedar as a billing viewer. A route map also shows a CSV export and a background refund worker. The developer says middleware checks the user's session on every route. The security engineer wants to know whether that is enough.
The expected policy is:
| Decision | Cedar billing viewer | Cedar billing manager | Member of another account |
|---|---|---|---|
| List Cedar invoice summaries | Allow, with approved summary fields | Allow | Deny |
| Read one Cedar invoice | Allow, excluding internal notes | Allow, excluding internal notes | Deny |
| Approve refund for Cedar invoice in refundable state | Deny | Allow within assigned limit | Deny |
| Export Cedar invoices | Allow only if separately granted | Allow only if separately granted | Deny |
The table is a review artifact, not a statement about any actual product. It exposes three questions hidden by “authenticated”: which account owns the invoice, which operation the role permits, and which fields the response may return. The manager's refund permission also needs a state and amount rule. A token containing account=cedar cannot prove an invoice belongs to Cedar; the server needs an authoritative invoice-to-account relationship. A client-supplied account or role value is an input to distrust, not a grant.
The first excerpt looks alarming. In this synthetic call graph, the HTTP handler selects an invoice by client-supplied ID:
GET /invoices/{id}
session = requireSession(request)
invoice = documents.readInvoice(session.context, id)
return invoiceView(invoice)
There is no visible account comparison in the handler. The reviewer opens documents.readInvoice instead of treating that omission as the conclusion:
readInvoice(context, id):
requireCurrentMembership(context.actor, context.account)
row = invoices.find(id, ownerAccount=context.account)
if row is absent: denyWithoutContent()
requireAction(context, "invoice.read", row)
return row
That mandatory lookup defeats the proposed foreign-account read for this call path. The account comes from verified context, and the stored relationship constrains the query. The reviewer records a rejected direct-read hypothesis, not a requirement to duplicate the same comparison in every HTTP handler.
Now inspect the export. It uses the same account-scoped document service, so it inherits meaningful object counterevidence. Its output takes a different branch:
detail: invoiceView(row) // number, total, due_date
export: csv(allStoredFields(row)) // also processor_internal_notes
For this example, the product policy explicitly excludes processor notes from both roles, even when they have export permission. The export's account filter is correct; its field projection is wrong. Neither “middleware checks login” nor “the document service checks ownership” answers that property question. The distinct finding candidate is an entitled exporter receiving a forbidden field from their own account's invoice.
This is a source reasoning exercise, not an executed request. The excerpts deliberately omit framework and storage syntax so the reader can see which fact defeats which claim. Production route mapping and any later output transformation still need to be established.
Trace one actor-to-effect path at a pinned revision
Use one immutable code revision and one declared deployment scope. For each route, record the actor and input → entry point → checks → resource operation → output or state change. A source path can establish what the reviewed code appears to do; it does not establish that an exploit was executed or that the route is deployed.
- Inventory the entry points. Ask the API owner for the route map, OpenAPI description, gateway rules, exports, batch operations, worker triggers and legacy versions. Search for every function that reads or changes the same object. Note unreviewed paths explicitly. A list check cannot defend a direct read unless that direct read necessarily passes through it.
- Establish the trusted caller. Identify where identity is verified, how the selected account is bound to the session, and whether internal calls carry an independently verified principal or narrow delegation. Trace impersonation, account switching, service credentials and jobs. If a worker receives
user_idortenant_idfrom a message body, find who created and authenticated that body. - Resolve the object from an authoritative relationship. Follow the invoice ID to its owning account and parent records. Look for a query constrained by both the selected account and ID, or a resource-level decision after lookup and before use. A UUID's unpredictability can reduce guessing; it is not an authorization decision, as OWASP's API1 guidance explains.
- Check the action and state. Read the rule that distinguishes viewing, exporting, approving and reversing. Include role, assignment, amount limit and current invoice status. Confirm that a stale cached role cannot outlive the organization's required revocation window. An
isAuthenticatedbranch does not answer these questions. - Check fields and side effects. Examine serializers, expansions, linked objects, error messages, counts and download responses. For writes, inspect accepted fields and server-owned values, including account IDs and approval state. API3 covers both unauthorized property reads and writes.
- Follow every delivery surface. An export, notification, job result, search index, object-store download or GraphQL resolver can bypass the reviewed HTTP handler. Ask whether each surface applies the same current policy to the same object and fields. OWASP's Authorization Decisions and Output Handling guidance explicitly extends restrictions to lists, counts and exports.
- Resolve and record. Seek the strongest counterevidence: a mandatory resource check deeper in the service, a deployment route that does not map to this code, a constraint that makes the harmful operation impossible, or a stronger independent data boundary. Preserve exact locations and the policy inputs they use. Then reject the specific claim, assign the missing evidence, or give engineering a source-supported repair target.
For Harbor Ledger, the path sheet now contains decisions rather than a second list of questions:
| Path / claim | Decisive evidence in synthetic HARBOR-42 | Decision and remaining boundary |
|---|---|---|
| Detail can read another account's invoice | Mandatory service lookup includes verified account and current membership | Reject that path; do not generalize to unreviewed loaders |
| Export can read another account's invoice | Export uses the same scoped service operation | Reject that object claim |
| Export reveals processor notes to an entitled exporter | CSV builder emits all stored fields; policy excludes the notes | Source-supported property disclosure candidate |
| Refund worker acts under stale authority | Worker artifact has not been supplied | Investigate separately; do not bundle into the export claim |
Notice what changed the result: inspecting the deeper operation rejected one allegation without making the whole API safe. A useful review is allowed to reduce its findings while increasing confidence in the one that remains.
Decide what the evidence supports
An authorization review should yield a decision per path, rather than one blanket verdict for an entire API:
| Status | Threshold | Next action |
|---|---|---|
| Rejected claim | A required control defeats the precise actor/action/object path in the reviewed scope. | Save the control, revision and route mapping. Review different paths separately. |
| Investigation | An essential policy, worker version, deployment mapping, object relationship or downstream check is unknown. | Name the missing artifact, owner and decision date. Consider containment if impact could be material. |
| Source-supported finding | The traced path permits an unauthorized read or change and relevant countercontrols have been checked. | Open a scoped repair; request independent review and runtime validation where they resolve material uncertainty. |
| Runtime-supported finding | Controlled evidence establishes the behavior on the identified build/configuration. | Update impact and deployment scope; verify the repair against the same rule. |
For this illustrative case, the export candidate advances to independent review. The worker stays an investigation. The direct-read claim is rejected for the traced operation. Those three dispositions can coexist in one review.
Hand off the surviving claim
The completed source handoff reads:
Actor: current Cedar member with the separate export grant. Action: export Cedar invoices through the reviewed CSV route. Policy: processor notes are excluded even for this actor. Mechanism: the account-scoped service returns a stored row, and the CSV builder chooses every stored field instead of the approved export projection. Direct effect supported by source: processor notes enter the generated CSV. Counterevidence resolved: mandatory ownership checks defeat a foreign-account read, but do not remove this column. Next evidence: deployed route/build mapping and an authorized synthetic export confirming the final CSV is delivered without a later projection. Repair owner: export service owner.
The proposed repair is an explicit export projection shared by every CSV path, with a deliberate decision for new stored fields. Its acceptance example contains a permitted invoice number and a synthetic processor note: the authorized export must retain the number and exclude the note. A foreign-account attempt must remain denied. An export containing no fields would satisfy neither the product contract nor meaningful fix verification.
Deployment evidence qualifies production claims; controlled validation qualifies observed behavior. Neither is a reason to postpone a well-supported, proportionate projection repair. For implementation decisions such as batch scope and all-or-nothing failure, continue with the REST authorization contract.
Keep new fields from silently changing the export contract
Harbor's explicit export projection should make adding a stored property a deliberate audience decision. A new database field need not become a new CSV column automatically. The export owner should identify its purpose and intended readers before including it in the projection.
This preserves the distinction established by the review: the document service can authorize the invoice correctly while the serializer releases too much. A source change to the data model is therefore a reason to revisit output selection, not to duplicate object checks that already work. The developer's acceptance record should retain useful invoice fields and exclude the synthetic processor note, with the account boundary preserved independently.
Start the next review by filling the policy matrix for one sensitive object, then tracing its direct read and list route. For a deeper list and cursor review, use pagination information leaks. When the concern is a side effect rather than a read, continue with checkout and credit-flow abuse paths.