Skip to content

content-lane + federated + APR: seven pre-flag defects, incl. remote text bypassing the public-safe scrub and peer instanceId takeover #9490

Description

@JSONbored

Summary

Defects in the content-lane surface-verification feature (#9255/#7670) and the federated peer-intelligence import path (#9243/#9148). Almost all are latent behind flags that currently ship disabled — which is exactly why they should be fixed now, before the flag flips, rather than discovered afterwards.

LOOPOVER_REVIEW_SURFACE_VERIFICATION ships "false" in wrangler.jsonc.

Content lane

1. Remote-server-controlled text reaches the public comment flagged as already-safe

src/review/content-lane/registry-logic.ts:652,656surface-verification.ts:277,333content-lane-wire.ts:94src/rules/advisory.ts:614,706src/review/unified-comment-bridge.ts:270.

probeFunctionalSurface interpolates the raw content-type header from the submitter's own server into detail ("not a json api surface (content-type:…)"). That string flows into the gate summary, which surfaceVerdictToGate marks alreadyPublicSafe: true — the exact flag that skips sanitizeForCheckRun and the CHECK_RUN_FORBIDDEN_TERMS scrub.

The wire's own invariant comment (:58-62, "summary comes solely from a fixed assessment vocabulary") was true before #9255 and is now false. A PR author's server answering with an arbitrary Content-Type gets that text posted verbatim by the bot.

  • Allowlist/truncate the echoed content-type at source (/^[\w.+-]+\/[\w.+-]+/), or drop alreadyPublicSafe for surface-lane findings.

2. MAX_PROBE_BODY_CHARS does not bound memory

src/review/content-lane/surface-verification.ts:144 does (await response.text()).slice(0, 64000) — the full hostile body is buffered before the slice. A streamed multi-hundred-MB response OOMs the isolate before the fail-closed path can run. The house pattern for a bounded reader already exists at src/review/grounding-wire.ts:230-249.

  • Use the bounded reader.

3. Unbounded probe fan-out

src/review/content-lane/orchestrator.ts:221-235 does Promise.all over every entry × 2 probes × up to 5 hops, with a per-hop 10 s timeout (surface-verification.ts:129) and maxAppendedEntries: Infinity (registry-logic.ts:934). Subrequest exhaustion converts into bulk probe_fetch_failed holds, and a 500-entry PR becomes a request-amplification primitive. The sibling source-evidence.ts:94 caps at 10.

  • Cap entries and total probes; bound total (not just per-hop) time.

4. Netuid grounding is self-corroborating

registry-logic.ts:283-286 matches netuid against targetText — the submitted URL's own page — with MIN_STRONG = 1. Owner and host require an independent source (:298-308); netuid does not. A submitter's own page asserting "netuid 64" therefore passes grounding.

  • Require an independent source for netuid, matching owner/host.

5. Key-order-sensitive entry identity

orchestrator.ts:71-77,99-105 keys entries on JSON.stringify(entry). Reordering an existing entry's keys reads as a fresh append and simultaneously removes its prior self from the duplicate check's scope — a path to unbounded content-free auto-merged registry PRs (contribution farming).

  • Canonicalize (sorted keys) before comparing.

Verified solid: redirect-hop SSRF re-check on every hop, fail-closed three-state verdict ordering, orchestrator aggregation (no error-passes-verdict path), safe-url.ts IP-literal coverage. Residual name-only SSRF (no DNS pin on Workers) is runtime-constrained and low.

Federated peer intelligence

6. instanceId takeover across allowlisted keys

src/orb/federated-import.ts:283-291 resolves in-batch conflicts last-wins by instanceId regardless of signing key, and :396-403 lets the watermark entry's keyFingerprint be overwritten by whichever key verified the newest bundle.

So a hostile-but-allowlisted peer B can claim honest peer A's instanceId: shadow A's bundle in-batch, then ratchet the watermark so A's genuine bundles are rejected as replayed_or_rollback — targeted suppression plus stat replacement. Taking over an existing id also bypasses B's MAX_INSTANCES_PER_KEY cap, since only new ids are counted (:405-409).

This sits squarely inside #9148's declared threat model ("bound the damage one still-trusted key can do").

  • Reject a bundle whose instanceId already exists under a different keyFingerprint, or namespace ids per key.

7. Unbounded instanceId length silently kills watermark persistence

Nothing bounds instanceId — the shape check at :138 only requires a string, and the pull body cap is 1 MB total (src/orb/ingest.ts:21). Roughly 3–4 bundles carrying ~600 KB ids push the system_flags state blob past D1's ~2 MB value limit, and writeFederatedPeerState (:343-349) swallows the failure forever. Replay and rollback watermarks then stop persisting for every peer, with no operator signal — silently regressing the #9148 protections.

  • Cap instanceId (e.g. 128 chars) in isBundleBodyShaped; log the write failure.

Verified solid: allowlist fail-closed, timing-safe non-early-exit key matching, range/window/freshness gates, the collector's bounded reader, MAX_PULLED_BUNDLES, the always-on rate bucket (#9166), and median-not-mean by construction.

Also: APR transfer has no tenant binding (currently inert)

src/api/routes.ts:3951-3959 + src/orb/apr-repo-transfer.ts:62-140. The only gate is ideaComplete, which is hard-false today (src/orb/apr-idea-completion.ts:27), so the route always 409s. But installationId, repoFullName and newOwner are all caller-supplied with no check that the repo is a loopover-org APR repo, that newOwner is the requesting customer, or that the installation matches the repo — and the module instructs implementers to "replace the body (keep the signature)".

The moment a persisted completion record lands, any authorized caller could transfer any completed APR repo — including another customer's — to themselves.

  • Put the authorization in requestAprRepoTransfer itself, now, not in the future route.
  • probeAprRepoTransfer:220 treats a plain 404 (repo deleted or uninstalled) as accepted_departed success — tighten.

(Repo creation is correctly implemented: it uses the customer's own OAuth session token, and the gate boolean cannot be smuggled — .strict(), #8000 P1 respected.)

Metadata

Metadata

Assignees

Labels

gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.maintainer-onlyOwner-only work — yields no Gittensor points.orbGittensory Orb related - maintainer self-hosting analytics.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions