Markers and evidence¶
A status code alone cannot tell you whether data moved. 200 where you
declared 404 is a disagreement with your declaration; it is not yet evidence
that the other identity saw anything. A marker closes that gap, and only
that gap.
This page covers what a marker is, where to put it, how Veridor looks for it,
which evidence grade each outcome gets, and the ways a marker fails to prove
anything. Every output below is from scripts/demo_server.py on port 18803:
python3 scripts/demo_server.py --port 18803 # patched
python3 scripts/demo_server.py --port 18803 --vulnerable # leaks inv-100, with the marker
python3 scripts/demo_server.py --port 18803 --vulnerable --no-marker-leak # serves inv-100, marker redacted
What a marker is¶
A marker is a high-entropy string that exists in one identity's own data and nowhere else. You declare it on that identity. When it turns up in a response sent to a different identity, that identity received the owner's data.
Veridor does not plant it. You put it in a record the owner owns, and the claim that it is unique to that owner is yours. Veridor tests that claim where it can (see uniqueness), but it cannot establish it.
A marker proves the string crossed. It is never proof of policy: whether the receiving identity was entitled to that data is what your declaration says. See the oracle proves data flow, never policy.
Generate one¶
Exit code 0. Each call prints a new value: vdr- followed by 24 random
URL-safe characters, checked against the same rules a configured marker is
held to before it is printed.
You may write your own instead. It is validated when the configuration loads, with no override:
veridor: identity 'invoice_owner' marker must be between 24 and 128 characters, got 11. Generate one with: veridor new-marker
Exit code 2, before any request. The full rule list (length, alphabet
A-Z a-z 0-9 _ -, at least 10 distinct characters, no character above 40% of
the length, no run of more than 4, no repeated pattern) is in the
configuration reference.
Where to plant it¶
Put it in a field of the owner's own record that the endpoint under test
returns: an invoice reference, a profile display name, a note title, a file's
contents. The demo plants vdr-Xk7Qm2Ls9Rt4Vb8Nc3Zp6Wf1 in the
customer_ref of invoice inv-100, which belongs to invoice_owner and no
other tenant.
Three conditions decide whether it works:
- The endpoint returns that field. A marker in a column the response never includes cannot be observed, and Veridor will say so rather than pass (see the field the endpoint does not return).
- Only the owner's data holds it. Not a build id, a shared configuration value, or anything every caller is served.
- Your request does not carry it. A request that sends the marker and gets it echoed back proves nothing, and is refused (see the reflected-marker refusal).
Declare it and refer to it¶
The marker is declared once, on the identity. Expectations refer to it by the owner's name, never by the value:
{
"version": 1,
"target": {"base_url": "http://127.0.0.1:8765", "timeout_seconds": 3},
"identities": {
"invoice_owner": {
"headers": {"Authorization": "Bearer owner-token"},
"marker": "vdr-Xk7Qm2Ls9Rt4Vb8Nc3Zp6Wf1"
},
"other_tenant": {
"headers": {"Authorization": "Bearer outsider-token"},
"marker": "vdr-Bq3Wm8Ke5Tz1Yx7Rn2Hd4Vs6",
"marker_probe": {"method": "GET", "path": "/api/invoices/inv-200"}
},
"administrator": {"headers": {"Authorization": "Bearer admin-token"}},
"anonymous": {"headers": {}}
},
"checks": [
{
"id": "invoice-tenant-isolation",
"request": {"method": "GET", "path": "/api/invoices/inv-100"},
"expect": {
"invoice_owner": {"status": [200], "contains_marker": "invoice_owner"},
"other_tenant": {"status": [404], "excludes_marker": "invoice_owner"},
"anonymous": {"status": [401]}
}
}
]
}
That is fixtures/veridor.demo.json with its name fields dropped and one of
its two checks shown.
| Field | On an expectation for identity X | Asserts |
|---|---|---|
contains_marker: "owner" |
usually X is the owner | X's response must carry owner's marker |
excludes_marker: "owner" |
X is anyone but the owner | X's response must not carry owner's marker |
Rules enforced when the file loads, each exit 2:
- One expectation may set one of the two, not both.
-
The named identity must exist and must declare a
marker: -
An expectation may not exclude its own identity's marker.
- Every
excludes_markerneeds a control (next section). - A near-miss key such as
contain_markerorexcludes-markeris refused rather than ignored, because a typo would silently switch the oracle off. See near-miss keys.
Every exclusion needs a control¶
Absence of a marker means something only if presence was possible. A response
that could never have carried the marker passes excludes_marker without
testing anything. So Veridor refuses an exclusion with nothing behind it:
veridor: check 'invoice-tenant-isolation' expectation for 'other_tenant' excludes_marker 'invoice_owner' requires a control in the same check; add {"invoice_owner": {"contains_marker": "invoice_owner", "status": [200]}} to check 'invoice-tenant-isolation' expect, or give identity 'invoice_owner' a 'marker_probe' naming a path where its own marker is served
Exit code 2. That configuration declared invoice_owner in the check with a
plain {"status": [200]} and no marker_probe. A plain status expectation is
not a control; the contains_marker assertion is.
There are two controls, and they prove different things.
The same-check control¶
The owner is probed at the very request under test and asserts its own
marker there, as invoice_owner does above. This proves the marker is
observable at this path, so its absence from the outsider's response is
falsifiable. Prefer it whenever the API allows.
If the owner is served without its own marker, the path cannot surface the
marker, and the exclusion is inconclusive rather than a pass. An identity
marker_probe cannot override that.
marker_probe¶
Some APIs put the caller's own principal in every path
(/fleet/v1/{device}/...), so the owner can never be sent the outsider's URL
and a same-check control is impossible. For those, declare a request the owner
should be served, on the identity:
"device_a": {
"headers": {"Authorization": "Bearer device-a-token"},
"marker": "vdr-OIItAlvvweFpwlDNmT1j5WJ8",
"marker_probe": {"method": "GET", "path": "/fleet/v1/device-a/artifacts/fw.bin"}
}
From fixtures/veridor.fleet.json. The probe takes the same keys as a check
request, runs once per run before any check, as the identity that owns the
marker, and is held to the same request rules, the reflection guard included.
It is a control, not a check: it produces no finding, is not counted in the
tally, and is recorded under marker_controls in the run JSON.
It is verified only when the request did not fail, the marker was observed,
and the response was not announced as served from a cache. Anything else
withholds the licence. A marker_probe on an identity with no marker is
refused at load time.
What a verified probe proves is narrower than the same-check control: the
marker is live somewhere, not that the path under test can surface it. A
run that leans on one says so. fixtures/veridor.fleet.json against the
patched demo:
…
Veridor run 20261110T043549Z-963411: 4/4 probes matched their declaration, 0 vulnerable, 0 unexpected denial, 0 inconclusive
…
Unproven oracle: 2 exclusion(s) were licensed by an identity marker_probe, which proves the marker is live elsewhere, not that the path under test can surface it:
- fleet-device-b-artifact / device_b: the absence of 'device_a''s marker in 'device_b''s response was licensed by 'device_a''s identity marker_probe, which proves the marker is live at http://127.0.0.1:18803/fleet/v1/device-a/artifacts/fw.bin -- not that this check's path can surface it
…
Exit code 0. The same disclosure is recorded under oracle_disclosures in
the run JSON and runs[0].properties.oracleDisclosures in SARIF.
A verified marker_probe also counts as proof that the identity's credential
is live, which matters for the liveness controls.
The owner probed without contains_marker¶
If the owner has a marker_probe and also appears in the check with a plain
status expectation, Veridor scans the owner's response at the check's path for
its own marker anyway. When the target serves the owner (a 2xx), that
response decides the question: marker present, the exclusion is licensed;
marker absent, it is not, and the marker_probe elsewhere cannot license it.
When the owner is refused or the request fails, the marker_probe is used.
How Veridor scans a response¶
- The scan runs in memory over the response body and the response headers. Bodies and headers are never written to disk.
- It matches the marker in ASCII, UTF-16LE and UTF-16BE, case-insensitively. The body is lowercased for the comparison and is never re-encoded; only the marker gets the UTF-16 variants.
gzipanddeflatebodies are decoded first, every member of a concatenated gzip stream included. Veridor sendsAccept-Encoding: identity, so this applies only to a server that compresses anyway.- A header match counts the same as a body match.
Content-Disposition,Location,ETagand debug headers are all ways data leaves.
Each scanned probe records a marker_scan of present, absent or
unknown. In the run JSON, probes[].response carries it with
marker_encodings (which needle matched) and marker_channel (body or
header). Two entries of probes from the patched run, reduced to those
fields:
{"check_id": "invoice-tenant-isolation", "identity": "invoice_owner", "response": {"status": 200, "marker_scan": "present", "marker_scan_reason": null, "marker_encodings": ["ascii"], "marker_channel": "body"}}
{"check_id": "invoice-tenant-isolation", "identity": "other_tenant", "response": {"status": 404, "marker_scan": "absent", "marker_scan_reason": null, "marker_encodings": [], "marker_channel": null}}
A probe whose expectation names no marker, such as anonymous here, is not
scanned, and its marker_scan is null.
unknown is never read as absence. Its marker_scan_reason says why:
| Reason | Means |
|---|---|
encoding_unsupported |
a Content-Encoding other than identity, gzip or deflate |
decode_failed |
the compressed stream could not be decoded, including a stream that ended mid-member |
incomplete_body |
the body ended short of its declared length |
truncated |
more than 8 MiB of decoded body; the rest was not scanned |
request_not_sent |
the request was never issued |
Evidence grades¶
Every finding carries an evidence_grade. The grade depends on whether the
observed status was one you declared, and on what the marker scan found. For a
check expectation, _grade_check in core.py assigns them in this order.
The status is outside the declared set (and is an answer from the authorization layer):
| Expectation | Marker scan | Grade | Rule, verdict |
|---|---|---|---|
excludes_marker |
present, and the fabricated credential also got it |
suspected |
VERIDOR-AUTHZ-001, vulnerable |
excludes_marker |
present |
confirmed |
VERIDOR-AUTHZ-001, vulnerable |
excludes_marker |
absent or unknown, or the control failed |
suspected |
VERIDOR-AUTHZ-001, vulnerable or unexpected_denial |
no excludes_marker |
not graded | ungraded |
VERIDOR-AUTHZ-001, vulnerable or unexpected_denial |
ungraded applies to an expectation with contains_marker too: only an
exclusion grades a status mismatch. The verdict is unexpected_denial when the
grade is not confirmed and the status is not 2xx.
The status is one you declared:
| Expectation | Observed | Grade | Rule, verdict |
|---|---|---|---|
excludes_marker |
present; the fabricated credential was served (2xx) and also got it |
suspected |
VERIDOR-LEAK-001, vulnerable |
excludes_marker |
present; the fabricated credential was not served (any non-2xx) and also got it |
n/a |
VERIDOR-ORACLE-001, inconclusive |
excludes_marker |
present |
confirmed |
VERIDOR-LEAK-001, vulnerable |
| either | response announced as from a cache | n/a |
VERIDOR-CACHE-001, inconclusive |
contains_marker |
not present |
n/a |
VERIDOR-ORACLE-001, inconclusive |
excludes_marker |
unknown, or the control is not present |
n/a |
VERIDOR-ORACLE-001, inconclusive |
excludes_marker |
absent, control present |
none | matched |
Not an answer. A transport error is always VERIDOR-EXEC-001,
inconclusive, n/a. A 429, a 5xx or a 407 outside the declared set is
the same, with one exception: an excludes_marker whose marker is present is
graded as above, because a leak inside a 500 is still a leak. A 429, 5xx
or 407 you declared is graded as a declared status.
Two consequences worth knowing:
- An observed marker outranks a failed control. A
marker_probethat was not verified withholds the licence for an absence; it does not stop a marker that was actually seen from gradingconfirmed. inconclusivedoes not trip--fail-on.--require-completeturns it into exit3. See exit codes.
One check, four targets¶
The invoice-tenant-isolation check from fixtures/veridor.demo.json, run
four ways:
Patched. Exit 0:
…
Veridor run 20261110T043346Z-f72e85: 6/6 probes matched their declaration, 0 vulnerable, 0 unexpected denial, 0 inconclusive
Target: http://127.0.0.1:18803
Regression diff: 0 new, 0 resolved, 0 unchanged
Evidence: 0 confirmed, 0 suspected, 0 ungraded, 0 escalated since the baseline
…
--vulnerable. The outsider is served inv-100, customer_ref and all.
Exit 1:
…
Veridor run 20261110T043401Z-6ff8c0: 5/6 probes matched their declaration, 1 vulnerable, 0 unexpected denial, 0 inconclusive
…
Evidence: 1 confirmed, 0 suspected, 0 ungraded, 0 escalated since the baseline
…
- [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: 'invoice_owner''s declared marker was observed in 'other_tenant''s response. That is proof the string crossed, and proof of data flow insofar as the marker is unique to 'invoice_owner''s data. You declared that 'other_tenant' must not receive 'invoice_owner''s data, and Veridor measured that it did. Whether that declaration is correct is not something Veridor can check.
The finding's evidence.marker in the run JSON:
{"mode": "excludes", "owner_identity": "invoice_owner", "state": "present", "reason": null, "control_state": "present", "encodings_matched": ["ascii"], "control_source": "check", "control_reason": null}
control_source is check for the same-check control, check_observed for
the owner probed without contains_marker, identity_probe for a
marker_probe, and none when nothing established the marker.
--vulnerable --no-marker-leak. The outsider is still served inv-100,
with customer_ref redacted. Same status, no marker. Exit 1:
…
Veridor run 20261110T043409Z-8d9017: 5/6 probes matched their declaration, 1 vulnerable, 0 unexpected denial, 0 inconclusive
…
Evidence: 0 confirmed, 1 suspected, 0 ungraded, 0 escalated since the baseline
…
- [vulnerable (suspected)] invoice-tenant-isolation / other_tenant: Identity 'other_tenant' received HTTP 200 (declared: one of [404]). 'invoice_owner''s marker was not observed (response did not contain it), so data exposure is unproven.
The default gate, new+confirmed, fails on this because the finding is new.
The same run with --fail-on confirmed exits 0.
No marker at all. The same check with the markers and marker assertions
removed, against --vulnerable. Exit 1:
{
"version": 1,
"target": {"base_url": "http://127.0.0.1:18803", "timeout_seconds": 3},
"identities": {
"invoice_owner": {"headers": {"Authorization": "Bearer owner-token"}},
"other_tenant": {"headers": {"Authorization": "Bearer outsider-token"}}
},
"checks": [
{
"id": "invoice-tenant-isolation",
"request": {"method": "GET", "path": "/api/invoices/inv-100"},
"expect": {
"invoice_owner": {"status": [200]},
"other_tenant": {"status": [404]}
}
}
]
}
…
Veridor run 20261110T043532Z-57f134: 1/2 probes matched their declaration, 1 vulnerable, 0 unexpected denial, 0 inconclusive
…
Evidence: 0 confirmed, 0 suspected, 1 ungraded, 0 escalated since the baseline
…
- [vulnerable] invoice-tenant-isolation / other_tenant: Identity 'other_tenant' received HTTP 200 (declared: one of [404]).
An ungraded finding prints its verdict with no grade beside it.
Uniqueness: the fabricated credential¶
confirmed rests on your premise that the marker is unique to its owner.
Veridor tests that premise where it can. For every GET, HEAD or OPTIONS
request that carries an excludes_marker, whatever statuses it accepts, run
sends one extra request carrying a credential it fabricated
(veridor-sham-…, which authenticates to nothing), and scans that response for
each excluded owner's marker. This is one of the
liveness controls.
If the fabricated caller gets the marker back, the marker is not owner-specific,
and the grade drops as the tables above show: suspected or inconclusive,
never confirmed. The message ends:
… 'invoice_owner''s marker is therefore not unique to its data, so the string crossing proves nothing about data flow. Plant a marker that appears only in that identity's records.
The demo target serves no marker to an unknown credential, so this message is
quoted from the source, not from a run on this page. The full message is in
what confirmed actually establishes.
Not getting the marker back is not proof of uniqueness. Where no fabricated
credential is sent, or its answer could not be scanned, the run says so. With
--no-prove-liveness against --vulnerable, exit 1:
veridor: liveness controls SKIPPED (--no-prove-liveness). …
…
Marker uniqueness not checked: 1 excludes_marker expectation(s) whose marker no credential Veridor fabricated was scanned for, so a marker that crosses there is graded on the premise that it is unique to its owner:
- GET /api/invoices/inv-100 (invoice_owner's marker): --no-prove-liveness was passed
Evidence: 1 confirmed, 0 suspected, 0 ungraded, 0 escalated since the baseline
…
The run JSON records each one under liveness.marker_uniqueness_not_checked
with a reason:
reason |
Why no fabricated credential was compared |
|---|---|
unsafe_method |
the request's method can change state |
prove_liveness_off |
--no-prove-liveness was passed |
no_credential_header |
no identity declares a credential header to shape one on |
control_errored |
the fabricated credential's request failed |
control_unscannable |
its response could not be scanned for the marker |
The reflected-marker refusal¶
If Veridor sends the marker and the target echoes it, the marker turns up in the response without any data crossing. So a configuration whose own request carries a marker is refused when it loads:
{
"id": "invoice-search",
"request": {"method": "GET", "path": "/api/invoices?customer_ref=vdr-Xk7Qm2Ls9Rt4Vb8Nc3Zp6Wf1"},
"expect": {
"invoice_owner": {"status": [200], "contains_marker": "invoice_owner"},
"other_tenant": {"status": [404], "excludes_marker": "invoice_owner"}
}
}
veridor: identity 'invoice_owner' marker appears in the request sent by check 'invoice-search'; a reflected marker cannot prove a leak
The check sits in a configuration with the invoice_owner and other_tenant
identities shown earlier. Exit code 2. Writing it as customer_ref=%76dr-Xk7Qm2Ls9Rt4Vb8Nc3Zp6Wf1
gets the same refusal. The check covers the method, the base URL, the path,
header names and values, and the body, after percent, plus, HTML entity,
\uXXXX, base64 and hex decoding to a bounded depth. A marker_probe request,
a session login step and a session inject entry are checked the same way.
A value a sequence captured at run time is
checked just before it is sent. If a capture carries a planted marker, the step
that would interpolate it is not issued, and the sequence is inconclusive
with marker_reflection_blocked.
To search by a value, search by an id or another field the target does not echo the marker in.
Failure modes¶
A marker that is not unique¶
Two identities with the same marker, or one marker inside another, are refused at load time, because a match has to name exactly one owner:
veridor: identities 'invoice_owner' and 'other_tenant' share the same marker; a match must identify exactly one owner. Generate one with: veridor new-marker
veridor: identity 'invoice_owner' marker is a substring of identity 'other_tenant' marker; a match must identify exactly one owner. Generate one with: veridor new-marker
Both exit 2. The comparison ignores case.
A marker that is unique in your configuration but not in the target's data, such as a value served to every caller, is only caught at run time, and only where a fabricated credential was sent and got it back (above). Where none was, a crossing is graded on your premise alone and the console says so.
A marker in a field the endpoint does not return¶
The demo's /fleet/v1/device-a/manifest.json serves device_a its device
metadata, without the artifact that holds the marker. A check that relies on it
as a same-check control:
{
"version": 1,
"target": {"base_url": "http://127.0.0.1:18803", "timeout_seconds": 3},
"identities": {
"device_a": {
"headers": {"Authorization": "Bearer device-a-token"},
"marker": "vdr-OIItAlvvweFpwlDNmT1j5WJ8"
},
"device_b": {"headers": {"Authorization": "Bearer device-b-token"}}
},
"checks": [
{
"id": "device-a-manifest",
"request": {"method": "GET", "path": "/fleet/v1/device-a/manifest.json"},
"expect": {
"device_a": {"status": [200], "contains_marker": "device_a"},
"device_b": {"status": [403], "excludes_marker": "device_a"}
}
}
]
}
…
Veridor run 20261110T043532Z-7f7c35: 0/2 probes matched their declaration, 0 vulnerable, 0 unexpected denial, 2 inconclusive
…
- [inconclusive] device-a-manifest / device_a: Identity 'device_a' received the declared HTTP 200, but 'device_a''s marker was not observed (response did not contain it); exclusion results for this check are unverified.
Confirm the marker is stored in the identity's data and returned by this endpoint, then re-run.
- [inconclusive] device-a-manifest / device_b: Identity 'device_b' received the declared HTTP 403, but the absence of 'device_a''s marker could not be verified (the control probe did not observe its own marker).
Confirm the marker is stored in the identity's data and returned by this endpoint, then re-run.
Exit code 0; with --require-complete, 3. Both statuses matched, and
neither probe is counted as a pass: the outsider's 403 could not have carried
the marker either way.
A marker_probe aimed at the same endpoint fails closed the same way. With
"marker_probe": {"method": "GET", "path": "/fleet/v1/device-a/manifest.json"}
on device_a, a check that device_b's artifact excludes device_a's marker,
against the patched demo, exit 0:
…
- [inconclusive] fleet-device-b-artifact / device_b: Identity 'device_b' received the declared HTTP 200, but the absence of 'device_a''s marker could not be verified (the marker probe for 'device_a' returned HTTP 200 without the marker, so the marker is not known to be live).
…
Against --vulnerable, where device_b is served device_a's artifact, the
same configuration grades [vulnerable (confirmed)] and exits 1: an
observed marker does not need the control.
The fix in both cases is to plant the marker in a field this endpoint returns, or aim the control at one that does.
A body that could not be scanned¶
A body in an unsupported encoding, a stream cut short, or one past the 8 MiB
scan ceiling reports unknown, never absent. With a declared status, the
exclusion is inconclusive; with an undeclared one, the finding is
suspected and its message says the body could not be fully scanned.
A response from a cache¶
When a response announces it came from a cache (Age, X-Cache,
CF-Cache-Status and the like), the marker result describes the object at
some earlier time. A probe with a marker assertion and a declared status is
VERIDOR-CACHE-001, inconclusive. An observed leak still grades as a leak.
marker_owner in the access matrix¶
veridor map scans every response for one identity's marker, named by
matrix.marker_owner. If exactly one identity declares a marker, it is used
without being named. If more than one does and marker_owner is not set, the
run is refused:
veridor: 2 identities declare a marker (invoice_owner, other_tenant); set matrix.marker_owner to name the one every response is scanned for.
Exit code 2. With "marker_owner": "invoice_owner", as in
fixtures/veridor.map-demo.json, against --vulnerable:
veridor map --config fixtures/veridor.map-demo.json --openapi fixtures/openapi.demo.json --base-url http://127.0.0.1:18803
…
[marker_reach] candidate -- other_tenant on GET /api/invoices/{id} [specimen owner_invoice]
other_tenant reached GET /api/invoices/{id} and the response carried
invoice_owner's planted marker. Data belonging to another named identity
was observed in other_tenant's response. This is an inconsistency in the
observations, not a finding. Declare the expectation to test it.
…
Exit code 0: nothing map finds is a finding. Without a marker owner, no
cell is ever marker_scan: present, marker_reach cannot fire, and drift
loses marker_appeared. See the matrix section in full.
What is written down¶
The marker literal is not written to the run JSON, the matrix, or any report.
grep -c vdr- over the --vulnerable run JSON and its matrix returned 0
for both, against 2 for fixtures/veridor.demo.json itself. A finding
records the owner's name, the scan state, the matched encoding and the channel.
A capsule is the one
exception, by design: verify needs the marker to match it. Response bodies are
scanned in memory and discarded; see credentials and markers.