You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Authorize resourceSubscriptions on subscriptions/listen #1307
On the modern server, subscriptions/listen accepts any URI in resourceSubscriptions. SubscriptionsFeature never checks that the caller is allowed to access it. Change events are server-wide, so on a multi-tenant server a client can subscribe to another principal's URI and receive notifications/resources/updated for it. Only timing and existence leak, not content.
Desired outcome
Server implementers have a hook to gate subscriptions per URI.
Example, non-normative implementation:
Add a canSubscribe(ctx, uri) hook to the resource repositories, defaulting to allow. subscriptions/listen drops every URI it denies.
Notes
Filtering by find isn't suitable: subscribing to a resource that doesn't exist yet is legitimate.
List-changed notifications have no payload and go to every listener. Document on McpChangeFeed that they aren't scoped per principal.
Thanks Daniel - appreciate the fast triage and the credit on #1307. The canSubscribe(ctx, uri) hook is the right shape; happy to review the PR against the cross-session cases when it's up.
The hook is the right shape, and one detail matters more than the default: allow-by-default fails open, so the hook should be able to distinguish three outcomes rather than two. Allowed, denied, and not-yet-existing. Your point that filtering via find is unsuitable is the correct constraint, and it is worth writing down explicitly, because an implementer reading only "drop the denied URIs" will reach for find and break forward-declared subscriptions.
Two things I would add to the proposal.
The denial path needs to be non-enumerable. Dropping a denied URI silently and returning an error for it are observably different from a client's side, and either one combined with a timing difference leaks existence even when content does not. Pick one behaviour for both denied and nonexistent and apply it uniformly.
Put the gate on whatever resolves URIs, not on the subscriptions/listen handler. If the check lives in the route, the next bulk subscribe or renamed endpoint becomes a second place to forget it. A single enforcement point that every subscription path calls also makes the audit story simple: one function, one default, one place to flip it.
Documenting on McpChangeFeed that list-changed notifications are not per-principal is worth doing even after the hook lands, since that distinction is invisible in the notification payload and easy for multi-tenant implementers to assume away.
Agree with all three, and the non-enumerable denial path is the important one - it closes the exact residual the issue calls out ("only timing and existence leak"). If denied and not-yet-existing return the same shape and comparable timing, there's no oracle left to work with.
One addition for the enforcement-point discussion: canSubscribe(ctx, uri) gates entry, but a subscription that was legitimately allowed can later become unauthorized while it's still live - a resource re-homed to another tenant, or an ACL change on an existing URI. If the only check is at subscribe time, the listener keeps receiving notifications/resources/updated after it should have lost access. So the single enforcement point media-ctrl describes should ideally be reachable from the dispatch path as well (re-check, or invalidate affected subscriptions on ACL/ownership change), not only from the subscribe path. Otherwise the hook is correct but revocation is a second, quieter version of the same cross-principal leak.
Happy to review the PR against both the subscribe-time and dispatch-time cross-session cases when it's up.
Activity
hackking007 commentedon Nov 3, 2026
Thanks Daniel - appreciate the fast triage and the credit on #1307. The canSubscribe(ctx, uri) hook is the right shape; happy to review the PR against the cross-session cases when it's up.
media-ctrl commentedon Nov 8, 2026
The hook is the right shape, and one detail matters more than the default: allow-by-default fails open, so the hook should be able to distinguish three outcomes rather than two. Allowed, denied, and not-yet-existing. Your point that filtering via
findis unsuitable is the correct constraint, and it is worth writing down explicitly, because an implementer reading only "drop the denied URIs" will reach forfindand break forward-declared subscriptions.Two things I would add to the proposal.
The denial path needs to be non-enumerable. Dropping a denied URI silently and returning an error for it are observably different from a client's side, and either one combined with a timing difference leaks existence even when content does not. Pick one behaviour for both denied and nonexistent and apply it uniformly.
Put the gate on whatever resolves URIs, not on the
subscriptions/listenhandler. If the check lives in the route, the next bulk subscribe or renamed endpoint becomes a second place to forget it. A single enforcement point that every subscription path calls also makes the audit story simple: one function, one default, one place to flip it.Documenting on
McpChangeFeedthat list-changed notifications are not per-principal is worth doing even after the hook lands, since that distinction is invisible in the notification payload and easy for multi-tenant implementers to assume away.hackking007 commentedon Nov 8, 2026
Agree with all three, and the non-enumerable denial path is the important one - it closes the exact residual the issue calls out ("only timing and existence leak"). If denied and not-yet-existing return the same shape and comparable timing, there's no oracle left to work with.
One addition for the enforcement-point discussion: canSubscribe(ctx, uri) gates entry, but a subscription that was legitimately allowed can later become unauthorized while it's still live - a resource re-homed to another tenant, or an ACL change on an existing URI. If the only check is at subscribe time, the listener keeps receiving notifications/resources/updated after it should have lost access. So the single enforcement point media-ctrl describes should ideally be reachable from the dispatch path as well (re-check, or invalidate affected subscriptions on ACL/ownership change), not only from the subscribe path. Otherwise the hook is correct but revocation is a second, quieter version of the same cross-principal leak.
Happy to review the PR against both the subscribe-time and dispatch-time cross-session cases when it's up.