Skip to content

_sub control topics lack subscription-ownership verification (cross-user unsubscribe/heartbeat) #115

Description

@fabracht

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.messagex-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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions