Case study: turning a BOLA/IDOR finding into a deterministic CI invariant #8
Pazificateur69
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
A typical BOLA finding starts with a simple observation: tenant B can read an object owned by tenant A when it knows, guesses, or reuses the object's identifier. The fix is usually small. The harder part is preserving the authorization boundary through later routing, ORM, middleware, and policy refactors.
The
pazent/exploitspec-demorepository is a minimal, runnable example of encoding that boundary as an observable contract instead of testing a particular implementation.The mechanism
The synthetic API has two identities:
ownerbelongs totenant-a;attackerbelongs totenant-b. The invariant performs this sequence:ownercreates a private invoice with a unique ID and payment reference generated at runtime.$.idand$.payment_referencefrom the response.attackerrequests the exact captured invoice ID using a separate actor session.403or404, must not contain the captured payment reference, and must not expose the invoice's tenant, customer, amount, or payment fields.The condensed core of the specification is:
Creating the object inside the test matters. A hard-coded object ID can pass because fixture data disappeared or because it never belonged to the owner in that environment. Capturing an ID from a successful owner request proves that the object existed immediately before the cross-tenant read. Checking both the denial status and the absence of sensitive fields prevents an empty
200or a partially redacted response from being mistaken for a preserved boundary.403and404are both accepted because applications make different legitimate choices about hiding object existence. The invariant is the security property—no cross-tenant read and no selected data leakage—not a preference for one policy's status code.The CI contract
The demo workflow builds the loopback-only API, runs this invariant, verifies the generated JUnit report has zero failures, and uploads it as an artifact. Its permissions are limited to
contents: read.The Action is pinned to the reviewed commit behind
v0.1.0, rather than a mutable tag:The other third-party Actions are SHA-pinned too, and Dependabot proposes updates for review. Here is a successful end-to-end run for the current demo commit.
Try the complete example
Requirements: Git and Go 1.25 or newer.
In a second terminal:
The reusable Action is also available on the GitHub Marketplace.
What this does—and does not—prove
A passing run proves that this request sequence, with these two identities and these assertions, preserved the tested boundary at that point in time. It does not prove that the application has no other BOLA paths, that list/update/delete/export endpoints are safe, or that no unasserted metadata or side channel exists.
ExploitSpec v1 executes ordered HTTP requests. It currently has no browser or JavaScript execution, automatic credential refresh, loops, retries, conditional steps, or cleanup/finally hooks. It also does not create or destroy test data automatically. For a real deployment, use an isolated test environment, synthetic data, dedicated least-privilege identities, narrow
allowed_hosts, and an explicit cleanup strategy.The demo intentionally ships a fixed, local-only API with inert tokens. It is an integration example, not a claim that it discovered a vulnerability in a real service.
Technical question: for a BOLA regression in your own stack, is the stable contract
403, an existence-hiding404, or either status combined with explicit non-leakage assertions—and how do you keep its test-data lifecycle deterministic in CI?All reactions