Skip to content

fix(discovery): harden upstream-OpenAPI discovery (size cap, cache, SSRF, pricing, ERC-8004)#779

Merged
bussyjd merged 1 commit into
integration/v0.14.0-rc0from
fix/discovery-openapi-hardening
Jul 16, 2026
Merged

fix(discovery): harden upstream-OpenAPI discovery (size cap, cache, SSRF, pricing, ERC-8004)#779
bussyjd merged 1 commit into
integration/v0.14.0-rc0from
fix/discovery-openapi-hardening

Conversation

@bussyjd

@bussyjd bussyjd commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Blockers #2 & #3 + discovery mediums from the PR #756 review. Caps the accepted upstream OpenAPI (~200KiB, falls back to the offer-scoped doc) so one offer can't brick the shared static-site ConfigMap for all offers; moves the fetch into a per-offer generation-keyed cache out of the shared rebuild path so a slow/flapping upstream can't convoy reconciles or roll the discovery pod; disables redirect-following and constrains the fetch to the offer's own namespace (confused-deputy SSRF); honors per-route price overrides in the upstream-derived x402.json; populates registrations[] so obol buy --expected-agent-id works on dedicated origins; tightens same-origin scoping; drops a dead check. 9 new tests.

https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk

… pricing, ERC-8004)

#777's upstream-OpenAPI discovery feature had six issues:

- getJSONMap accepted up to 8MiB and re-marshaled it into the shared
  "obol-skill-md" ConfigMap; anything over k8s's ~1MiB ConfigMap limit
  bricked static-site publishing for every offer. Cap fetch + re-marshal
  at 200KiB and fall back to buildOfferScopedOpenAPI when exceeded.

- tryUpstreamOpenAPI ran inline (up to 3x3s GETs, uncached) inside
  buildOfferBundles, itself called under staticSiteMu on every offer's
  reconcile. A slow/flapping upstream serialized reconciles and
  flip-flopped the content hash, rolling the shared discovery pod on
  every flap. Added upstreamOpenAPICache, keyed by offer UID+generation:
  refresh() runs from each offer's own reconcile outside staticSiteMu,
  at most once per generation; buildOfferBundles now takes a lookup func
  and only ever reads the cache.

- fetchUpstreamOpenAPI followed redirects and let Upstream.Namespace
  (offer-author-controlled) pick an arbitrary namespace, with none of
  the ReferenceGrant checks the Gateway API data path requires for
  cross-namespace targets. Disabled redirect following, pinned the
  probe to the offer's own namespace, and rejected non-simple
  openapiPath overrides (traversal / embedded scheme).

- buildOfferWellKnownX402FromOpenAPI priced every paid op at the
  offer's default, ignoring spec.routes[] price overrides the
  non-upstream path already honors. Added routePriceOverride, matching
  the verifier's own pattern semantics.

- buildOfferAgentRegistration never set registrations[], which ERC-8004
  requires once an offer is on-chain; buyers using --expected-agent-id
  failed closed. Populated from status.AgentID + the resolved payment
  network's registry.

- Services[].Endpoint origin scoping used HasPrefix(ep, origin), which
  admits an evil-suffix host like origin+".evil.tld". Fixed to exact
  match or origin+"/".

- Deleted an unreachable duplicate security-empty check in
  openAPIOpIsPaid.

Every fix has a test that fails without it (verified by reverting each
change in isolation and re-running its test).

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
@bussyjd
bussyjd merged commit aabd024 into integration/v0.14.0-rc0 Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant