Category: spec-conformance Severity: major
Location: Sources/ARCP/Envelope/MessageType.swift:65-70, Sources/ARCP/Runtime/ARCPRuntime.swift:329-336, Sources/ARCP/Runtime/SubscriptionManager.swift:75-86
Spec: ARCP v1.1 §7.6, §14 ("Subscription scope")
What
The spec's §7.6 subscription is a per-job attach (job.subscribe with job_id, from_event_seq, history; responds job.subscribed; MUST verify the principal may observe the target job, else PERMISSION_DENIED). The SDK has no job.subscribe/job.subscribed/job.unsubscribe types at all (MessageType only has the generic subscribe/subscribe.event). The generic subscribe handler (handleSubscribe) performs zero authorization: any authenticated session can subscribe with an empty filter and receive every envelope of every job of every principal flowing out of the runtime. This is the exact privilege-escalation vector §14 says runtimes MUST default-deny. Existing issue #69 mentions "job.subscribe performs no principal authorization" but the actual code has no job.subscribe path — the live generic subscribe is the unauthorized surface and is broader than #69 describes.
Evidence
case .subscribe(let payload):
try await handleSubscribe(envelope: envelope, payload: payload, info: info, transport: transport)
handleSubscribe calls subscriptionManager.subscribe directly with the client filter — no principal check, no ownerSessionId-vs-target comparison. SubscriptionRecord.matches filters only on session/trace/job/stream/type/priority, never on the subscriber's authorization to see those sessions.
Proposed fix
- In
handleSubscribe, enforce default "same principal only": reject (or silently scope) any filter whose sessionIds includes a session not owned by info.principal.subject. Without a session filter, restrict matching to the subscriber's own sessions unless deployment policy explicitly opts into cross-principal observation.
- Log every subscribe with subscriber principal, requested scope, and decision (§14 "Cross-session subscription audit").
- If §7.6
job.subscribe is in scope, add the message types and a handler that returns PERMISSION_DENIED for unauthorized principals.
Acceptance criteria
Category: spec-conformance Severity: major
Location:
Sources/ARCP/Envelope/MessageType.swift:65-70,Sources/ARCP/Runtime/ARCPRuntime.swift:329-336,Sources/ARCP/Runtime/SubscriptionManager.swift:75-86Spec: ARCP v1.1 §7.6, §14 ("Subscription scope")
What
The spec's §7.6 subscription is a per-job attach (
job.subscribewithjob_id,from_event_seq,history; respondsjob.subscribed; MUST verify the principal may observe the target job, elsePERMISSION_DENIED). The SDK has nojob.subscribe/job.subscribed/job.unsubscribetypes at all (MessageTypeonly has the genericsubscribe/subscribe.event). The genericsubscribehandler (handleSubscribe) performs zero authorization: any authenticated session can subscribe with an empty filter and receive every envelope of every job of every principal flowing out of the runtime. This is the exact privilege-escalation vector §14 says runtimes MUST default-deny. Existing issue #69 mentions "job.subscribe performs no principal authorization" but the actual code has nojob.subscribepath — the live genericsubscribeis the unauthorized surface and is broader than #69 describes.Evidence
handleSubscribecallssubscriptionManager.subscribedirectly with the client filter — no principal check, noownerSessionId-vs-target comparison.SubscriptionRecord.matchesfilters only on session/trace/job/stream/type/priority, never on the subscriber's authorization to see those sessions.Proposed fix
handleSubscribe, enforce default "same principal only": reject (or silently scope) any filter whosesessionIdsincludes a session not owned byinfo.principal.subject. Without a session filter, restrict matching to the subscriber's own sessions unless deployment policy explicitly opts into cross-principal observation.job.subscribeis in scope, add the message types and a handler that returnsPERMISSION_DENIEDfor unauthorized principals.Acceptance criteria