Problem/Motivation

Several cheap or anonymous requests fan out into calls to the billing provider with nothing limiting them, from a security review before beta7:

  • The user-insert hook synchronously queries every installed connector by email on every registration. Registration is anonymous and core does not flood-control it.
  • Each login of a user without a subscription enqueues a remote lookup, and nothing dedupes queue items by user; repeated logins (which core does not flood-limit when they succeed) queue unbounded lookups for cron.
  • With the login-redirect setting on, every login instantiates and polls every connector on the request path.
  • Every GET to /subscription-manager/subscribe, api/subscribe-url, api/portal-url and the manage route of a hosted-portal connector mints a fresh remote checkout or portal session; SubscriptionRefreshThrottle deliberately leaves these paths alone.

A scripted account, or a registration loop, can exhaust a provider's rate limit or quota and litter it with sessions. The module side is confirmed; the cost depends on the connector.

Proposed resolution

  • Registration: move the sync off the request into the same queue the login path uses, so a registration storm costs the queue, not the provider.
  • Login: dedupe the queue by user with a short-lived flag in the refresh throttle's cache (one pending lookup per user), and route the login-redirect poll through the throttle so a user's connectors are polled at most once per window.
  • Session minting: a per-user, per-route flood window (core's flood service, default 10 per 10 minutes, settings), after which the route returns 429 with a Retry-After header and the API shapes an error. Hosted-portal and checkout sessions are short-lived at the provider, so a bounded count per window loses nothing.
  • Kernel tests for each limit and the queue dedupe; the flood settings on the settings form.

Remaining tasks

  • Design pass on the limits and whether the 429 should instead redirect to the management page; implementation; tests; change record.

User interface changes

Flood settings on the settings form.

API changes

The subscribe and portal API routes can return 429. No connector interface changes.

Data model changes

None.

Comments

colan created an issue.

colan’s picture

Audit note: the module already declares subscription_manager.refresh_throttle (SubscriptionRefreshThrottle, cache plus time) in the services file, but nothing in src/ uses it and its docblock describes a route that does not exist. Whatever this issue decides should either build on that class or remove the declaration; the dead-code cleanup issue defers that call here.

colan’s picture

Scope addition. Commit e41b3bf on 1.0.x added a subscription_manager.tools service exposing five Tool API tools to AI agents, MCP clients and ECA. Two of them make remote calls on every invocation and belong on this issue's list:

  • subscription_manager.refresh_subscription: one refreshSubscription() call to the subscription's connector per invocation. Gated by update access on the subscription, so administrators only, but an agent acting for an administrator can loop it.
  • subscription_manager.sync_plans: syncPlans() across one or every connector per invocation. Gated by administer subscription plans.

Whatever shape the throttle takes here (the unused SubscriptionRefreshThrottle service already declared, or something new), these two entry points should go through it alongside the registration, login, subscribe and portal paths, so an agent cannot drive a connector's rate limit on an administrator's behalf. The three read-only tools (list plans, list a user's subscriptions, list overdue subscriptions) are local queries and need nothing.