Problem
The federated peer-benchmark import trusts peer-supplied values far more than its own comments claim.
Four compounding gaps, all in the same path.
1. One allowlisted key mints unlimited Sybil peers and sets the median to any value
verifyFederatedBundle accepts a bundle if it verifies under any allowlisted key, and nothing binds a
key to an instanceId — the code explains why it can't ("instanceId is unauthenticated until a key
verifies"). importPeerBundles (src/orb/federated-import.ts:128-136, :176-183) then pushes every
verifying bundle into accepted with no dedup on instanceId, and the benchmark does
accepted.map(b => b.mergePrecision).filter(≠null).sort() → percentile(…, 50).
So a holder of one allowlisted key signs 99 bundles with 99 distinct instanceIds and owns the median
outright. The header comment at :10-13 asserts the opposite — "a Sybil attack is self-limiting by
construction: forging peers requires the RECEIVING operator to have added the attacker's keys
themselves" — which is true of keys and false of peers. The companion defence ("MEDIAN, NOT MEAN — a
bounded number of outliers cannot drag a median") assumes a bound this code never enforces.
Two aggravating details: peerCount is documented as "how many peers actually contributed" but counts
bundles (peerMergePrecisions.length), so the card renders "99 peers" for one; and the doc's claim
that only peers "that itself cleared MIN_DECIDED" contribute is enforced nowhere — a peer self-reports
decided: 3, mergePrecision: 1.0 and it counts.
2. Fields are finiteness-checked but never range-checked
isBundleBodyShaped's numeric predicate (federated-import.ts:75-94) is
typeof value === "number" && Number.isFinite(value). Nothing constrains mergePrecision to [0,1],
decided/windowDays to positive, or cycleP95Ms to sane. A signed bundle carrying
mergePrecision: -3 or 1e308 passes rejectionFor unchanged and lands in the median.
3. No freshness or replay protection
generatedAt and windowDays are signed but never read: rejectionFor checks schema version, shape,
and signature — nothing else. No freshness window, no monotonicity against a previously-seen bundle from
the same instance, no persisted state. A collector — explicitly untrusted ("a hostile collector is
indistinguishable from 'no peers yet'", federated-collector.ts:169-170) — can re-serve one favourable
year-old bundle forever, and mixing windowDays: 7 with windowDays: 365 bundles into one median is
uncaught despite that field's own docstring ("so peers only median equal-length windows").
4. The peer response has no byte ceiling, and is fetched inside a request
federated-collector.ts:180-185 does const payload: unknown = await response.json() with no
Content-Length pre-check, no streamed cap, and no array-length cap — only a 5s AbortSignal.timeout.
Every sibling reader in this subsystem has one: readOrbIngestBody (1 MiB), readOrbRelayRegisterBody
(4 KiB), boundedResponseText (2 KB). And the only production caller is buildFederatedBenchmark at
src/api/routes.ts:1716 — inside the awaited body of the maintainer dashboard GET, which therefore
also pays the full 5s latency on every load whenever the collector is slow.
Impact
The maintainer-facing "you are behind the peer median" verdict becomes attacker-chosen, or simply
nonsense. Display-only today (no gate path — verified), but it is decision-support. Plus a memory/stall
vector on an authenticated route.
Requirements
- Key
accepted by instanceId (last bundle per instance wins) and cap contributions per verifying key.
- Enforce
decided >= MIN_DECIDED receiver-side instead of trusting the sender's null.
- Range-validate every numeric field in
isBundleBodyShaped.
- Reject bundles whose
generatedAt falls outside a bounded window of now; require windowDays to
match the local window; persist a per-instance high-water mark to detect rollback.
- Read the response through the same bounded reader the ingest path uses; cap array length; move the pull
off the request path onto the background tick.
- Fix
peerCount to count instances, and correct the two header comments that overstate the defences.
Test Coverage Requirements
99%+ patch coverage, branch-counted; adversarial tests for N-bundles-one-key, out-of-range values, a
stale generatedAt, and an oversized response.
Links & Resources
src/orb/federated-import.ts ~10-13, ~75-94, ~128-144, ~176-183; src/orb/federated-collector.ts ~169-185;
src/orb/federated-benchmark.ts ~53-66; src/api/routes.ts ~1715-1720
- Feature is currently inert (
federatedIntelligence absent from the live manifest) but wired —
buildFederatedBenchmark runs on every maintainer dashboard load, so one YAML key arms it.
maintainer-only — federated trust boundary.
Problem
The federated peer-benchmark import trusts peer-supplied values far more than its own comments claim.
Four compounding gaps, all in the same path.
1. One allowlisted key mints unlimited Sybil peers and sets the median to any value
verifyFederatedBundleaccepts a bundle if it verifies under any allowlisted key, and nothing binds akey to an
instanceId— the code explains why it can't ("instanceIdis unauthenticated until a keyverifies").
importPeerBundles(src/orb/federated-import.ts:128-136,:176-183) then pushes everyverifying bundle into
acceptedwith no dedup oninstanceId, and the benchmark doesaccepted.map(b => b.mergePrecision).filter(≠null).sort()→percentile(…, 50).So a holder of one allowlisted key signs 99 bundles with 99 distinct
instanceIds and owns the medianoutright. The header comment at
:10-13asserts the opposite — "a Sybil attack is self-limiting byconstruction: forging peers requires the RECEIVING operator to have added the attacker's keys
themselves" — which is true of keys and false of peers. The companion defence ("MEDIAN, NOT MEAN — a
bounded number of outliers cannot drag a median") assumes a bound this code never enforces.
Two aggravating details:
peerCountis documented as "how many peers actually contributed" but countsbundles (
peerMergePrecisions.length), so the card renders "99 peers" for one; and the doc's claimthat only peers "that itself cleared MIN_DECIDED" contribute is enforced nowhere — a peer self-reports
decided: 3, mergePrecision: 1.0and it counts.2. Fields are finiteness-checked but never range-checked
isBundleBodyShaped's numeric predicate (federated-import.ts:75-94) istypeof value === "number" && Number.isFinite(value). Nothing constrainsmergePrecisionto[0,1],decided/windowDaysto positive, orcycleP95Msto sane. A signed bundle carryingmergePrecision: -3or1e308passesrejectionForunchanged and lands in the median.3. No freshness or replay protection
generatedAtandwindowDaysare signed but never read:rejectionForchecks schema version, shape,and signature — nothing else. No freshness window, no monotonicity against a previously-seen bundle from
the same instance, no persisted state. A collector — explicitly untrusted ("a hostile collector is
indistinguishable from 'no peers yet'",
federated-collector.ts:169-170) — can re-serve one favourableyear-old bundle forever, and mixing
windowDays: 7withwindowDays: 365bundles into one median isuncaught despite that field's own docstring ("so peers only median equal-length windows").
4. The peer response has no byte ceiling, and is fetched inside a request
federated-collector.ts:180-185doesconst payload: unknown = await response.json()with noContent-Lengthpre-check, no streamed cap, and no array-length cap — only a 5sAbortSignal.timeout.Every sibling reader in this subsystem has one:
readOrbIngestBody(1 MiB),readOrbRelayRegisterBody(4 KiB),
boundedResponseText(2 KB). And the only production caller isbuildFederatedBenchmarkatsrc/api/routes.ts:1716— inside the awaited body of the maintainer dashboard GET, which thereforealso pays the full 5s latency on every load whenever the collector is slow.
Impact
The maintainer-facing "you are behind the peer median" verdict becomes attacker-chosen, or simply
nonsense. Display-only today (no gate path — verified), but it is decision-support. Plus a memory/stall
vector on an authenticated route.
Requirements
acceptedbyinstanceId(last bundle per instance wins) and cap contributions per verifying key.decided >= MIN_DECIDEDreceiver-side instead of trusting the sender's null.isBundleBodyShaped.generatedAtfalls outside a bounded window ofnow; requirewindowDaystomatch the local window; persist a per-instance high-water mark to detect rollback.
off the request path onto the background tick.
peerCountto count instances, and correct the two header comments that overstate the defences.Test Coverage Requirements
99%+ patch coverage, branch-counted; adversarial tests for N-bundles-one-key, out-of-range values, a
stale
generatedAt, and an oversized response.Links & Resources
src/orb/federated-import.ts~10-13, ~75-94, ~128-144, ~176-183;src/orb/federated-collector.ts~169-185;src/orb/federated-benchmark.ts~53-66;src/api/routes.ts~1715-1720federatedIntelligenceabsent from the live manifest) but wired —buildFederatedBenchmarkruns on every maintainer dashboard load, so one YAML key arms it.maintainer-only — federated trust boundary.