Surfaced by PR #114 (#40), which makes $DB/_sub/# publishable by any authenticated non-admin. Confirmed by a counter-test (two non-admin MQTT connections) and independent analysis.
The gap
The _sub control handlers do not verify that the caller owns the subscription:
handle_subscribe(db, payload) (crates/mqdb-agent/src/agent/handlers.rs ~697) records no owner — it calls db.subscribe(pattern, entity) with no caller identity.
Subscription (crates/mqdb-core/src/subscription.rs:16) has no owner/sender field: {id, pattern, entity, share_group, mode}.
handle_heartbeat(db, sub_id) / handle_unsubscribe(db, sub_id) (~741/749) act on a bare sub_id lookup with no caller check.
sender_uid IS available in the admin dispatch (AdminContext.message → x-mqtt-sender, same as the CRUD path at handlers.rs:202-207) but is not threaded into these three handlers.
Result: any authenticated user who learns another user's sub_id can unsubscribe (cancel) or heartbeat (keep alive) that subscription.
Reproduced
Two non-admin connections, no admin users:
A subscribe -> {"id":"8f1a687f-..."}
B (different user) publish $DB/_sub/8f1a687f-.../unsubscribe -> {"ok":true}
A heartbeat after -> 404 entity not found: subscription id=8f1a687f-...
Current severity: LOW
sub_id is a random UUIDv4 returned only to the owner's response topic; it is not in ChangeEvent and $DB/_sub/# subscribe is denied — so it isn't observable over MQTT.
- The
_sub registry is currently inert: dispatcher::add_listener has no callers, so _sub/subscribe registrations don't drive event delivery (the CLI receives events via its own $DB/{entity}/events/# subscription). Removing a registry entry therefore doesn't stop the victim's actual delivery today.
Escalation conditions
- If the
_sub registry is ever wired to delivery (a real add_listener call), cross-user unsubscribe becomes a trivial DoS on another user's stream.
- If a
sub_id leaks (server logs, a future list/introspection endpoint, a shared/guessable response topic), the "id is secret" guard collapses.
- Minor:
heartbeat returning ok-vs-404 is a validity oracle (negligible at UUIDv4 entropy).
Proposed fix
Give Subscription an owner (the authenticated uid), set it in handle_subscribe from the dispatch sender_uid, and verify it in handle_heartbeat/handle_unsubscribe (reject with forbidden when the caller is not the owner; admins/internal-service may bypass). Also consider whether the currently-inert _sub registry should be wired or removed.
Acceptance
- A subscription created by user A cannot be unsubscribed/heartbeated by user B (non-owner) — returns forbidden.
- Owner (and admin) can still unsubscribe/heartbeat.
- Test covering cross-user rejection at the handler level.
Surfaced by PR #114 (#40), which makes
$DB/_sub/#publishable by any authenticated non-admin. Confirmed by a counter-test (two non-admin MQTT connections) and independent analysis.The gap
The
_subcontrol handlers do not verify that the caller owns the subscription:handle_subscribe(db, payload)(crates/mqdb-agent/src/agent/handlers.rs~697) records no owner — it callsdb.subscribe(pattern, entity)with no caller identity.Subscription(crates/mqdb-core/src/subscription.rs:16) has no owner/sender field:{id, pattern, entity, share_group, mode}.handle_heartbeat(db, sub_id)/handle_unsubscribe(db, sub_id)(~741/749) act on a baresub_idlookup with no caller check.sender_uidIS available in the admin dispatch (AdminContext.message→x-mqtt-sender, same as the CRUD path athandlers.rs:202-207) but is not threaded into these three handlers.Result: any authenticated user who learns another user's
sub_idcanunsubscribe(cancel) orheartbeat(keep alive) that subscription.Reproduced
Two non-admin connections, no admin users:
Current severity: LOW
sub_idis a random UUIDv4 returned only to the owner's response topic; it is not inChangeEventand$DB/_sub/#subscribe is denied — so it isn't observable over MQTT._subregistry is currently inert:dispatcher::add_listenerhas no callers, so_sub/subscriberegistrations don't drive event delivery (the CLI receives events via its own$DB/{entity}/events/#subscription). Removing a registry entry therefore doesn't stop the victim's actual delivery today.Escalation conditions
_subregistry is ever wired to delivery (a realadd_listenercall), cross-userunsubscribebecomes a trivial DoS on another user's stream.sub_idleaks (server logs, a future list/introspection endpoint, a shared/guessable response topic), the "id is secret" guard collapses.heartbeatreturning ok-vs-404 is a validity oracle (negligible at UUIDv4 entropy).Proposed fix
Give
Subscriptionanowner(the authenticated uid), set it inhandle_subscribefrom the dispatchsender_uid, and verify it inhandle_heartbeat/handle_unsubscribe(reject with forbidden when the caller is not the owner; admins/internal-service may bypass). Also consider whether the currently-inert_subregistry should be wired or removed.Acceptance