Skip to content

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

veridor new-marker
vdr-62HeSEG8ZRvWQzICak8NR-pj

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:

    veridor: check 'invoice-tenant-isolation' expectation for 'invoice_owner' contains_marker references identity 'invoice_owner', which defines no 'marker' field. Generate one with: veridor new-marker
    
  • An expectation may not exclude its own identity's marker.

  • Every excludes_marker needs a control (next section).
  • A near-miss key such as contain_marker or excludes-marker is 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.
  • gzip and deflate bodies are decoded first, every member of a concatenated gzip stream included. Veridor sends Accept-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, ETag and 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_probe that was not verified withholds the licence for an absence; it does not stop a marker that was actually seen from grading confirmed.
  • inconclusive does not trip --fail-on. --require-complete turns it into exit 3. See exit codes.

One check, four targets

The invoice-tenant-isolation check from fixtures/veridor.demo.json, run four ways:

veridor run --config fixtures/veridor.demo.json --base-url http://127.0.0.1:18803

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 --config veridor.json
…
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 --config veridor.json
…
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.