All research

API authorization.

Authentication is not authorization. A reproducible, multi-tenant API fault-injection study.

Read the study

Abstract

We designed a deterministic multi-tenant HTTP API and an 18-invariant authorization test suite. The suite covers cross-tenant read/write/delete boundaries, authorization context derived from trusted identities, tenant-scoped collection results, function-level role boundaries, and object-property assignment restrictions. We ran the same cases against one hardened configuration, three individually fault-injected variants, and one combined fault configuration: 90 individual HTTP-level test observations.

The hardened variant satisfied 18/18 invariants. The variants with deliberately removed object checks, role/property checks, or trusted-context enforcement each violated at least four invariants, while the combined fault variant violated 11. This supports a narrow engineering conclusion: this specific invariant suite detects the deliberately injected authorization regressions in the included synthetic application. It does not measure the prevalence of vulnerabilities in production APIs, demonstrate a new zero-day, or prove these fault patterns are exploitable in any third-party product.

Research question

Can a small, testable suite of API authorization invariants distinguish a correctly scoped multi-tenant service from services with three common classes of access-control regressions, while preserving positive use cases and anonymous-authentication checks?

This experiment does not ask whether real organizations are vulnerable to these exact bugs; establishing that would require individually authorized assessments of production software and a representative sample.

Threat model

The synthetic service has two tenants (alpha and beta), multiple document owners and administrators, and five synthetic user identities. It supports document read, edit and deletion; collection listing; an administrator-only users endpoint; and an internal-notes endpoint. All records and bearer tokens are artificial strings. The HTTP server binds only to 127.0.0.1, uses an ephemeral port, and never reaches external systems.

An authenticated caller from alpha is considered adversarial for the purposes of negative tests when attempting to read or alter beta documents, modify a peer's document, elevate access to an administrator-only function, submit protected object properties, or override a tenant attribute through untrusted HTTP inputs. The authenticated caller's original tenant identity remains authoritative in the hardened mode.

Experimental method

We reuse identical seed data and the same set of 18 test cases for all five variants. After every test request, application data is reset to an initial deep copy so that mutations cannot influence another case. Each observation records the HTTP response, expected policy, pass/fail decision, and before/after document state differences.

Variant Fault model
secure Checks trusted tenant, caller's role/ownership, and a property allowlist.
object Deliberately removes document-level tenant checks and document-list filtering.
function Deliberately removes role/ownership guards and permits protected-property assignment.
context Deliberately treats X-Tenant-ID or ?tenant= supplied by the caller as trusted authorization context.
combined Deliberately enables all three fault classes together.

Test oracles. For negative controls, a request must be denied with the stated status and not modify stored data. For positive write controls, the approved title change must actually occur. For collection read tests, the visible object/user IDs must exactly match the requester's tenant. Status codes chosen in this lab (such as 403 vs. 404) are not mandatory for all real production APIs; implementations should adapt expected responses to their own security policy.

The test harness uses Python's http.client for actual HTTP requests and http.server as the lab target. No third-party modules or Internet access are required.

Measured results

Variant Invariants passed Invariants violated Failed case IDs
secure 18 / 18 0 —
object 11 / 18 7 C08, C09, C10, C11, C12, C13, C18
function 14 / 18 4 C14, C15, C16, C17
context 14 / 18 4 C11, C12, C13, C18
combined 7 / 18 11 C08–C18

Aggregate: 90 HTTP observations; 26 violated invariants across the four deliberately insecure variants and zero in the reference configuration. The harness detected all four seeded fault configurations (4/4), but this must not be interpreted as a general detection-rate or accuracy measurement across real-world applications.

Cross-tenant access

In object mode, the test suite identifies cross-tenant reads and cross-tenant modifications/deletion by an administrator of a different tenant (C08–C10). Document listing also leaks foreign records in this mode (C11–C12). The test design intentionally includes a user who is an administrator within the wrong tenant: checking only whether a caller is an administrator does not establish which tenant's records they may modify.

Untrusted tenant selection

In context mode, an alpha user can redirect list behavior by adding an arbitrary tenant query parameter or X-Tenant-ID header (C11–C12). The same untrusted header can redirect access and an administrator's edit to a foreign tenant (C13, C18). In the secure variant, these caller-supplied values do not alter the tenant associated with the authenticated identity. This is a demonstration of a seeded design mistake, not a claim about a proxy or framework's defaults.

Roles & protected properties

In function mode, a member can reach administrator-only endpoints or internal fields and may edit another member's document (C14–C16). A permitted title-edit route also accepts protected fields (tenant, owner, approved) that the hardened implementation rejects (C17). This illustrates why an allowlist of mutable fields and function-level authorization must complement object-level tenant filtering.

Beyond authentication

C06 and C07 reject missing and invalid credentials in every variant. Nonetheless, the object, function and context variants violate multiple authorization invariants after authenticating a valid user. This is a controlled counterexample to treating successful authentication tests as proof of adequate authorization.

Recorded observations

Choose a configuration, then open a case to inspect its recorded response and state changes. “Satisfied” means the observed behavior met that case’s expected policy.

18 satisfied · 0 violated

C01owner can read own documentSatisfied
Category
positive control
Expected HTTP status
200
Observed HTTP status
200

Response and state changes

{
  "response": {
    "document": {
      "approved": false,
      "id": "101",
      "owner": "alice",
      "tenant": "alpha",
      "title": "Alpha planning"
    }
  },
  "state_mutations": []
}
C02same tenant member may read peer documentSatisfied
Category
positive control
Expected HTTP status
200
Observed HTTP status
200

Response and state changes

{
  "response": {
    "document": {
      "approved": false,
      "id": "102",
      "owner": "charlie",
      "tenant": "alpha",
      "title": "Alpha operations"
    }
  },
  "state_mutations": []
}
C03owner can update ordinary titleSatisfied
Category
positive control
Expected HTTP status
200
Observed HTTP status
200

Response and state changes

{
  "response": {
    "document": {
      "approved": false,
      "id": "101",
      "owner": "alice",
      "tenant": "alpha",
      "title": "Authorized update"
    }
  },
  "state_mutations": [
    {
      "after": "Authorized update",
      "before": "Alpha planning",
      "document": "101",
      "field": "title"
    }
  ]
}
C04tenant admin may update peer documentSatisfied
Category
positive control
Expected HTTP status
200
Observed HTTP status
200

Response and state changes

{
  "response": {
    "document": {
      "approved": false,
      "id": "102",
      "owner": "charlie",
      "tenant": "alpha",
      "title": "Authorized admin update"
    }
  },
  "state_mutations": [
    {
      "after": "Authorized admin update",
      "before": "Alpha operations",
      "document": "102",
      "field": "title"
    }
  ]
}
C05tenant admin user list scoped to tenantSatisfied
Category
positive control
Expected HTTP status
only_alpha_users
Observed HTTP status
200

Response and state changes

{
  "response": {
    "users": [
      {
        "id": "alice",
        "role": "member",
        "tenant": "alpha"
      },
      {
        "id": "charlie",
        "role": "member",
        "tenant": "alpha"
      },
      {
        "id": "admin-alpha",
        "role": "admin",
        "tenant": "alpha"
      }
    ]
  },
  "state_mutations": []
}
C06missing credentials rejectedSatisfied
Category
authentication
Expected HTTP status
401
Observed HTTP status
401

Response and state changes

{
  "response": {
    "error": "unauthenticated"
  },
  "state_mutations": []
}
C07invalid token rejectedSatisfied
Category
authentication
Expected HTTP status
401
Observed HTTP status
401

Response and state changes

{
  "response": {
    "error": "unauthenticated"
  },
  "state_mutations": []
}
C08cross-tenant document read blockedSatisfied
Category
API1:2023 BOLA
Expected HTTP status
403
Observed HTTP status
403

Response and state changes

{
  "response": {
    "error": "outside tenant"
  },
  "state_mutations": []
}
C09cross-tenant admin document modification blockedSatisfied
Category
API1:2023 BOLA
Expected HTTP status
403
Observed HTTP status
403

Response and state changes

{
  "response": {
    "error": "outside tenant"
  },
  "state_mutations": []
}
C10cross-tenant admin document deletion blockedSatisfied
Category
API1:2023 BOLA
Expected HTTP status
403
Observed HTTP status
403

Response and state changes

{
  "response": {
    "error": "outside tenant"
  },
  "state_mutations": []
}
C11query supplied tenant cannot redirect document listSatisfied
Category
untrusted context
Expected HTTP status
only_alpha_docs
Observed HTTP status
200

Response and state changes

{
  "response": {
    "documents": [
      {
        "approved": false,
        "id": "101",
        "owner": "alice",
        "tenant": "alpha",
        "title": "Alpha planning"
      },
      {
        "approved": false,
        "id": "102",
        "owner": "charlie",
        "tenant": "alpha",
        "title": "Alpha operations"
      }
    ]
  },
  "state_mutations": []
}
C12header supplied tenant cannot redirect document listSatisfied
Category
untrusted context
Expected HTTP status
only_alpha_docs
Observed HTTP status
200

Response and state changes

{
  "response": {
    "documents": [
      {
        "approved": false,
        "id": "101",
        "owner": "alice",
        "tenant": "alpha",
        "title": "Alpha planning"
      },
      {
        "approved": false,
        "id": "102",
        "owner": "charlie",
        "tenant": "alpha",
        "title": "Alpha operations"
      }
    ]
  },
  "state_mutations": []
}
C13header supplied tenant cannot permit foreign document readSatisfied
Category
untrusted context
Expected HTTP status
403
Observed HTTP status
403

Response and state changes

{
  "response": {
    "error": "outside tenant"
  },
  "state_mutations": []
}
C14ordinary member denied admin functionSatisfied
Category
API5:2023 BFLA
Expected HTTP status
403
Observed HTTP status
403

Response and state changes

{
  "response": {
    "error": "admin role required"
  },
  "state_mutations": []
}
C15ordinary member denied internal notesSatisfied
Category
API5:2023 BFLA
Expected HTTP status
403
Observed HTTP status
403

Response and state changes

{
  "response": {
    "error": "admin role required"
  },
  "state_mutations": []
}
C16non-owner member denied title editSatisfied
Category
API5:2023 BFLA
Expected HTTP status
403
Observed HTTP status
403

Response and state changes

{
  "response": {
    "error": "write denied"
  },
  "state_mutations": []
}
C17protected document properties cannot be mass assignedSatisfied
Category
API3:2023 BOPLA
Expected HTTP status
400
Observed HTTP status
400

Response and state changes

{
  "response": {
    "error": "protected fields",
    "fields": [
      "approved",
      "owner",
      "tenant"
    ]
  },
  "state_mutations": []
}
C18untrusted tenant header cannot let foreign admin editSatisfied
Category
untrusted context
Expected HTTP status
403
Observed HTTP status
403

Response and state changes

{
  "response": {
    "error": "outside tenant"
  },
  "state_mutations": []
}

Engineering recommendations

  1. Derive tenant context from a verified identity, not a caller-controlled query value or a client-supplied header. If a gateway supplies tenant identity, enforce a verifiable trusted boundary and strip spoofable inbound values.
  2. Enforce row/object tenant policy on every read, write, delete and list query, not only on entry pages or route groups.
  3. Check role and, when applicable, record ownership separately from tenant scope. A role is not a universal cross-tenant authorization.
  4. Explicitly allowlist fields for each mutation. Treat ownership, tenant attribution and approval state as protected business-policy fields.
  5. Test negative cases with postconditions and collection-content checks; HTTP status alone may miss data exposure or side effects.
  6. Preserve positive controls so over-restrictive policies do not masquerade as secure behavior.

Reproduce the study

Source availability. This release includes the report and recorded observations. The original Python study source and CI configuration are not included. The downloads support inspection of the recorded results; independent reruns require the study source.

python --version
python run_study.py

The runner exits with an error if the reference hardened variant fails any invariant or if a fault-injected variant violates none. Evidence is stored in evidence/results.json (all 90 request observations, including state differences) and evidence/results.csv (compact table). The Python runtime version and UTC experiment timestamp are recorded in JSON; read the source for exact route logic and fixtures.

This experiment was run with Python 3.13.5 in an isolated Linux container. It uses only the standard library. Different supported Python versions are checked by the included GitHub Actions configuration once the repository is publicly hosted and CI is enabled.

Validity & open questions

  • Synthetic application: the examined targets are deliberate test fixtures, not independent production deployments. The study says nothing about the frequency or impact of vulnerabilities outside this lab.
  • Fault selection: all vulnerabilities were planted by the study author. Catching four planned variants does not establish discovery performance for unknown bug classes, and invariant failures overlap across variants.
  • Scale and adversarial realism: this is a small deterministic scenario, not a broad fuzzing campaign, distributed system, database-backed SaaS product, reverse proxy, or full adversarial red-team exercise.
  • Security outcome: zero failures in the hardened case only means it passes the 18 specified tests, not that it is free of security defects.
  • External verification: a public release with signed/attributed source, CI, and an independently repeated run would strengthen confidence. A real-world validation against an authorized, versioned owned application would substantially strengthen the evidence.

References

Evidence downloads

Download the technical report, complete JSON observations, or a CSV summary. SHA-256 checksums let you verify each file.

Download SHA-256 checksums

Evidence recorded with Python 3.13.5 on . The scope is a synthetic application on loopback; these findings are not a production vulnerability prevalence study.

More from Aspirehive Research