Skip to content

Security (latent): GraphQL and realtime surfaces must satisfy identity admission before a client transport ships — both drop/lack the caller context today #2992

Description

@os-zhuang

Surfaced by the ADR-0096 E4 verification. Two execution surfaces are not exploitable today (neither is wired to a client), but both would fall open the instant a real transport is added — loaded guns that the ADR-0096 D4 admission test exists to catch. Tracking them so the identity story is fixed before wiring, not after an adversarial review.

Surface 1 — GraphQL (stub today, latent context-drop)

  • No GraphQL engine exists: no graphql npm dependency anywhere, and kernel.graphql is never assigned in the monorepo. Every POST /graphql throws 501 before any engine call. The only IGraphQLService is a dev stub returning { data: null, errors: [...] } (plugin-dev/src/dev-plugin.ts:99-106). So: SAFE now.
  • Latent defect: handleGraphQL (packages/runtime/src/http-dispatcher.ts:1488-1499) passes only { request: context.request } to kernel.graphql(...) — it drops context.executionContext, unlike the REST callData path (:332, :1944-1974) which threads it as options.context. One entry point (dispatcher-plugin.ts:570-572) even bypasses dispatch() entirely, so resolveExecutionContext never runs. The moment anyone implements a real kernel.graphql that resolves objects through ObjectQL, it runs context-less = fall-open (full authority).
  • Pre-wiring fix: thread { request, context: context.executionContext } at :1497 and have the resolver forward it as options.context; gate /graphql behind the same requireAuth/enforceAuth used by handleMetadata (:1587-1595). A tiny preemptive patch is cheap and closes the landmine now.

Surface 2 — realtime/websocket (no transport, no per-recipient authz seam)

  • No end-user transport is wired: handleUpgrade (WebSocket handshake) is unimplemented, there is no REST subscribe route (packages/rest/src has zero realtime refs), and the client (packages/client/src/realtime-api.ts) is an explicit placeholder (@ts-expect-error … Reserved for future WebSocket/SSE). Only two trusted server-internal plugins subscribe (webhooks auto-enqueuer, knowledge sync). So: SAFE now, no external subscriber.
  • Structural defect: the delivery path is a pure fan-out with no seam for per-recipient authorization. InMemoryRealtimeAdapter.publish (service-realtime/src/in-memory-realtime-adapter.ts:78-89) delivers to every matching subscription; matchesSubscription (:149-166) filters only by object name + event type. The Subscription struct (:13-18) carries no principal — there is nothing to check a row against. RealtimeSubscriptionOptions.filter is declared but never read. Grep for authorize|canRead|rls|sharing|fls|tenant|userId|owner across service-realtime/src: no matches.
  • Payload is the full record, not an id: engine emits created (objectql/src/engine.ts:2291-2294) and updated (:2462-2466) with the full after row. So a future subscriber would receive record bodies directly, cross-tenant, that a normal find would hide.
  • Pre-wiring fix: before any WebSocket/SSE subscribe path ships, add a per-recipient RLS/FLS/tenant re-check on delivery (subscription must carry a principal), or switch the payload to id-only + client re-fetch under its own RLS. Also delete/rewrite service-realtime/README.md, which advertises authorizeChannel/broadcastToUser/presence-auth that do not exist.

Why track rather than fix-now

Neither is a live data path, so there's no active breach and no urgency to patch behavior. But both are exactly the D4 admission-test targets: register each as a matrix row now with its posture ("stub — must thread caller identity / add per-recipient authz before wiring"), so the CI meta-test blocks anyone from wiring a transport without the identity story. The GraphQL one-line context-thread is cheap enough to do preemptively; the realtime one needs the authz seam designed alongside the transport.

Relationship to ADR-0096

E4 "unknown seams" resolved: both SAFE-now / latent. Seeds two D4 conformance-matrix rows (state experimental/UNKNOWN → now stub, pre-wiring). Refs: ADR-0096 (#2975 / e07645c), #2849.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions