Research
Application Security

The Anatomy of Authorization Failures

S. Iqbal 12 min

Why object-level checks keep slipping through review, and how to test for them systematically.

Why this keeps happening

Most findings in this class are not exotic. They appear when a boundary that exists in the design is not enforced in the code path that actually runs. Reviews miss them because the control lives one layer away from the request handler, and tests assert on behaviour rather than on authorization.

Everything below is written for defenders. Apply it only to systems you own or are explicitly authorized to test.

A defensible pattern

Push the check to the data access layer, make it impossible to query without an identity, and assert on it in tests. The snippet below sketches the shape of that control.

// authorization enforced where the data is read
async function getInvoice(userId: string, invoiceId: string) {
  const row = await db.invoice.findFirst({
    where: { id: invoiceId, ownerId: userId }, // ownership is part of the query
  });
  if (!row) throw new NotFoundError(); // do not leak existence
  return row;
}

Severity guidance

ConditionImpactPriority
Unauthenticated access to recordsFull data exposureCritical
Cross-tenant readConfidentiality breachHigh
Verbose error reveals existenceEnumerationMedium
Missing audit trailSlower responseLow

Takeaways

  • Make the secure path the shortest path for developers.
  • Test authorization as a first-class requirement, not an edge case.
  • Log denials with enough context to detect probing without leaking data.