Skip to content

orb(security): the 'registered instance' trust gate on the published guarantee authenticates nothing — instance_id is body-supplied, and an absent arm remotely DELETEs the live guarantee #9121

Description

@JSONbored

Problem

The "registered instance" trust gate protecting the published ≥95% close guarantee authenticates
nothing. The instance identity it checks is a plain field of the caller's own request body.

handleOrbIngest (src/orb/ingest.ts) destructures the identity from the payload:

121:  const { instance_id, events, health } = payload as OrbIngestPayload;

and then uses that attacker-supplied string as the entire trust anchor for the risk-control write:

211:  // #8835: ... TRUST GATE: stored only when the sender is a REGISTERED instance — a published accuracy
212:  // guarantee is the strongest claim on the homepage, and open ingest must not let a stranger plant one.
218:  const registeredRow = await db.prepare("SELECT registered FROM orb_instances WHERE instance_id = ?").bind(instance_id).first...
219:  if (registeredRow?.registered === 1) {

Nothing binds the presented credential to the claimed instance_id:

  • isAuthorizedIngest (src/api/routes.ts ~6626) compares a single fleet-wide ORB_INGEST_TOKEN,
    which every exporter in the fleet holds as ORB_COLLECTOR_TOKEN. It proves "some fleet member", never
    "this instance".
  • The sender's x-orb-signature is not verified by handleOrbIngest — it cannot be, since the
    collector does not hold the sender's anon secret (src/selfhost/orb-collector.ts ~226-229).
  • The id is not secret either: instanceId() is sha256(ORB_APP_ID).slice(0,16) over a small integer
    (orb-collector.ts:108-109), brute-forceable offline, and it is sent in clear as x-orb-instance.

So any holder of the shared collector token — any fleet member, or anyone who extracts it from any
self-host deployment — can present a registered instance's id and write the guarantee.

The retraction path is worse. The else arm fires whenever an arm is merely absent from the
payload:

227:  } else {
228:    // The sender no longer publishes this arm — retract the fleet copy too (stale guarantees lie).
229:    await db.prepare(`DELETE FROM system_flags WHERE key = ?`).bind(`riskcontrol:fleet:${arm}`).run();

A payload carrying risk_control: {} therefore deletes riskcontrol:fleet:close and
riskcontrol:fleet:merge. Both keys are global singletons, and loadFleetGuarantee
(src/review/public-stats.ts ~456-460) reads them straight onto /v1/public/stats — an endpoint
confirmed publicly reachable (HTTP 200 on api.loopover.ai, 2026-07-26).

Trigger

POST /v1/orb/ingest        Authorization: <shared collector token>
{ "instance_id": "<registered id>", "events": [], "health": {"ok": true},
  "risk_control": { "close": { "alpha": 0.05, "lambda": 0.99, "coverageAtLambda": 1.0, "nAtLambda": 9999 } } }

→ homepage guarantee becomes attacker-chosen. Send "risk_control": {} instead → guarantee retracted.

Impact

The strongest public claim the project makes is writable, and deletable, by any fleet peer rather than
only by the instance that earned it. This is the same claim an external evaluator is being pointed at.

Requirements

  • Derive the writer's identity from the credential, not the body: per-instance enrolment secret or
    per-instance token → instance_id. A body-supplied id may be accepted only when it matches the
    credential's own identity; a mismatch is a 403, not a silent accept.
  • Scope the flag per instance (riskcontrol:fleet:<instance>:<arm>) and aggregate at read time, so one
    compromised or buggy peer cannot overwrite the fleet's single global cell.
  • Require an explicit retraction (e.g. {"close": null}) rather than inferring retraction from an
    absent key, so a truncated, partial, or older-schema payload cannot silently delete a live guarantee.
  • Range/shape-validate the stored object rather than persisting it verbatim (see orb(data): fleet guarantee is a single last-writer-wins key with no validation, and gamingFlagsCaught is structurally always 0 #9068 for the
    validation half; this issue owns the authentication half).

Test Coverage Requirements

99%+ patch coverage, branch-counted. Both arms of the identity check (credential matches / mismatches
the claimed id), the explicit-retraction arm, and the absent-key arm (must now be a no-op, asserted
by a regression test naming this issue).

Links & Resources

Dedup

Distinct from #9068, which covers last-writer-wins semantics and missing validation on this key.
Even with #9068 fully fixed, registered gates nothing: the id is unauthenticated and enumerable, and
the DELETE-retraction path stays remotely reachable. #9068 is "the value is unvalidated"; this is "the
writer is unauthenticated".

Boundaries

Ingest authentication + flag scoping + retraction semantics. No change to how risk-control calibrates
locally, and no change to the guarantee's statistics.

maintainer-only — public trust-surface and fleet-credential authority.

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions