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
- 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.
- Enforce row/object tenant policy on every read, write, delete and list query, not only on entry pages or route groups.
- Check role and, when applicable, record ownership separately from tenant scope. A role is not a universal cross-tenant authorization.
- Explicitly allowlist fields for each mutation. Treat ownership, tenant attribution and approval state as protected business-policy fields.
- Test negative cases with postconditions and collection-content checks; HTTP status alone may miss data exposure or side effects.
- 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
- OWASP, API Security Top 10 — 2023.
- OWASP, API1:2023 Broken Object Level Authorization.
- OWASP, API5:2023 Broken Function Level Authorization.
- OWASP WSTG, API Broken Object Level Authorization.
Evidence downloads
Download the technical report, complete JSON observations, or a CSV summary. SHA-256 checksums let you verify each file.
- Full technical reportMarkdown · 10.4 KB
- Complete recorded evidenceJSON · 47.3 KB
- Observation tableCSV · 11.7 KB
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