Status: early alpha.
Your authorization boundary, as a regression test.
Broken access control, where a user can reach data or actions they should not, ranks first in the OWASP Top 10 and has the most recorded occurrences in OWASP's data. OWASP notes that automated scanning tools can spot a missing access check but cannot confirm that an existing one works, and it advises teams to write their own access control tests and not deploy changes that fail them. Veridor runs those tests in a team's build pipeline, so a change that breaks an access rule the team has declared and tested can fail the build before it ships.
Sources: OWASP Top 10:2025, A01 Broken Access Control; OWASP Top 10:2017, A5 Broken Access Control; OWASP API Security Top 10:2023, API1.
veridor map and veridor run against Veridor's demo API (scripts/demo_server.py), with and without --vulnerable. Paths are under /api. The fix changes one cell.What it does today
Declare what each identity is allowed to do. Veridor issues the same request as every identity, compares what happened against what you declared, and fails the build when the answer changes.
Three commands, graded verdicts, standard reports.
Sends the same read-only requests as each identity you give it and records who reached what. Its suggestions are leads to check, never findings.
Sends each request you declared as every identity, compares the answers with your access rules, and by default fails the build on any new or confirmed break.
Deliberately weakens your declared rules to show whether each of your checks can fail at all.
Graded by evidence: confirmed means another identity's planted marker came back in the response; suspected and ungraded say less, and say so.
SARIF, JUnit, GitLab SAST, Code Quality and Markdown.
The CI workflow passed every job on commit 38025ed (2026-11-08).
Published fixes
Built on published fixes.
Four access-control flaws found and reported by the author, each fixed by its project and published with a CVE record. Each one is now a test of Veridor.
- InvoiceShelf
The owner of one company could read the account details of a user who belonged only to another company. Fixed in 2.4.1.
CVE-2026-55610GHSA-vgx6-6cqr-m8qr - Eclipse hawkBit
A device could list and download firmware files assigned to a different device. Fixed in 1.0.4.
CVE-2026-16454GHSA-92r3-p8c2-3fpx - Ghost
An Editor could promote another staff user to Editor, a role Editors are not allowed to grant. Fixed in 6.64.0.
CVE-2026-105678CVE-2026-104412GHSA-4pvx-fwjj-8gpc - Domoticz
A user with view-only rights could run a server-side script command that the fix restricts to higher rights. Fixed in 2026.3.
CVE-2026-75520GHSA-8rv5-x8wv-c5c4
Each test runs the project's own published release from just before the fix and the one that fixed it, and passes only when Veridor reports the flaw on the first and nothing on the second. For InvoiceShelf and hawkBit, the proof is another account's data appearing in the response; for Ghost and Domoticz, it is the server allowing an action it should refuse. All four passed on 2026-11-09.
Roadmap
Next: a first release.
Status: early alpha.
- Next
Open source
Source published under Apache-2.0.
- Then
First release
A tagged, signed release.
- After
Packages
PyPI, container image and Helm chart.
- Later
Docs and listings
Docs site, GitHub Marketplace and GitLab CI/CD Catalog.
About
An independent project.
Built by Santosh Kumar Puppala, an independent security researcher. It draws on his published research on authorization flaws.
Technical detail · proof, not inference
A status code cannot tell you what came back.
Plant a unique marker in one identity's data. If another identity's response carries it, the bytes crossed.
-
You
Plant a marker in invoice_owner's record
specimen owner_invoice
field: marker -
Veridor
Send the same request as other_tenant
GET /api/invoices/{id}
credential: other_tenant -
Target
Answers with the record
HTTP 200 (declared: 404)
body contains marker -
Verdict
The owner's marker crossed
vulnerable (confirmed)
build exits 1
- confirmed
- another identity's marker was in the response
- suspected
- status differed; no owner-specific marker seen
- ungraded
- status differed; no marker configured
The product
Three verbs, asked in this order.
Discover, declare, gate, then verify the gate.
What would a compromise reach?
Credentials and a spec, nothing declared. Reach is a lower bound, counted in operations.
Observed reach (lower bound), identity "other_tenant": reached 1 refused 2 unresolved 0 unmeasured 2 of 5 no share is rendered: 2 operation(s) were not measured, … [marker_reach] candidate -- other_tenant on GET /api/invoices/{id} [specimen owner_invoice] EXIT=0 # nothing map finds fails a build
Does the boundary hold, and did it change?
The gate: graded findings, diffed against a baseline.
5/6 probes matched their declaration, 1 vulnerable, 0 unexpected denial, 0 inconclusive Evidence: 1 confirmed, 0 suspected, 0 ungraded, … - [vulnerable (confirmed)] invoice-tenant-isolation / other_tenant: Identity 'other_tenant' received HTTP 200 (declared: one of [404]) and the response carried 'invoice_owner''s marker … EXIT=1 # fixed demo: 6/6 probes matched, EXIT=0
Would your checks catch a regression?
Six operators rewrite your declarations. A survivor is a check that cannot fail.
Mutation score: 0.92 (12 killed / 13 generated) survived 1 aberrant 0 errored 0 not applicable 2 skipped 3 0.86 of 14 generated + limited (1 could not be evaluated) [SURVIVED] M1 invert-denial -- check 'widget-read-permissive', identity outsider EXIT=1 # one mutant survived
A regression check with nothing declared.
Not a separate command. It sees only what changed, in cells both runs measured.
Access-matrix drift — 2 change(s), 2 that mean more access drift saw 60% of the grid: 12 of 20 comparable cell(s) [reach_opened] GET /api/invoices/{id} other_tenant [specimen owner_invoice] refused -> reached [marker_appeared] GET /api/invoices/{id} other_tenant [specimen owner_invoice] absent -> present EXIT=1
Measured, with bounds
Two numbers, two different questions.
scripts/noise_floor_study.py, seeds 1 to 9, ten runs each. Drawn to scale.
fired a false drift gate on a server whose boundary never changes
95% CI [0.0%, 4.1%] (Wilson)
One synthetic server, one noise profile. Not proof of zero.
caught one planted boundary opening
96.7% · 95% CI [90.7%, 98.9%]
One fixture. 0% where the baseline did not measure the cell.
Neither is a detection rate for authorization bugs in general. Separately, CI requires the verdict to flip across a set of published fixes, including the four listed above.
Built for the pipeline
Bounded, and it says unknown.
0 third-party runtime imports: an AST scan of all 25 source files, which catches a planted import.
A probe that drew a 429, a 5xx or a timeout is inconclusive. With --require-complete, which every integration turns on, it exits 3; without it, only a run where no probe was answered exits 3.
GET and HEAD only, 2 requests a second by default, at most 10. run sends what you declared, unthrottled; --read-only refuses writes.
SARIF, JUnit, GitLab SAST, Code Quality, Markdown. No SARIF upload to code scanning observed yet.
CI/CD integrations
Where the gate has been run end to end.
By default every integration fails the job on any non-zero exit.
- 0 pass
- the selected gate passed
- 1 fail
- the gate failed: a finding the --fail-on policy selects
- 2 refused
- run: a configuration, coverage or safety refusal (mutate also uses 2 when nothing was measured or a mutant was unresolved)
- 3 incomplete
- gate passed, but something it needs went unmeasured, such as no probe answered (with --require-complete, any probe unanswered)
Also 70 (internal error, never a finding), 130 and 143 (interrupted; teardown runs).
- GitHub Action
Ran in a local harness (a vulnerable target fails the step, a fixed one passes) and on GitHub-hosted Linux and macOS runners in the project's CI.
local harness · GitHub runners - GitLab CI/CD component
The job's own rendered script, run in Docker against the demo API: 158 checks covering exits 0, 1, 2, 3 and 143 and every report (2026-09-27).
Docker - Argo CD PostSync gate (Helm)
A kind cluster with Argo CD v3.5.3: a vulnerable target fails the sync, a fixed one succeeds.
kind cluster - Kustomize plain Job
The same kind run: the vulnerable Job fails, the fixed one completes.
kind cluster - Argo Rollouts analysis
The same kind run with Argo Rollouts v1.10.0: a vulnerable canary is aborted, a fixed one promoted.
kind cluster - Container image
Built in CI and run as the gate, and as the job image in the GitLab run above.
CI - Zipapp
Built, then run on Python 3.11 against the demo API: vulnerable exits 1, fixed exits 0.
Python 3.11
Also provided: configuration for Jenkins, Azure Pipelines and AWS CodeBuild. For each, the gate's shell runs on a host against the demo API and the file is checked by that platform's linter, schema or reference. The Kubernetes entries ran on kind clusters; the GitLab, Jenkins, Azure and CodeBuild entries ran outside their own platform, as described.
What it does not claim
The limits are the product.
It consumes the policy you wrote; it cannot audit it.
- A finding means behaviour differed from your declaration, which may itself be wrong.
- Confirmed proves the marker crossed, not that your policy is right.
- Coverage says a cross-identity denial is asserted, never that it is enforced.
- A green build means nothing the gate fails on was found on this run.
- Not a scanner. No route discovery (map probes only the spec you give it), no policy invention, no fuzzing or payloads.
Install
From source, at release.
Python 3.11 or newer, standard library only. Install steps will be published with the source.