Active
Project:
Subscription Manager
Version:
1.0.x-dev
Component:
Code
Priority:
Major
Category:
Bug report
Assigned:
Unassigned
Reporter:
Created:
11 Oct 2026 at 04:33 UTC
Updated:
11 Oct 2026 at 04:35 UTC
Jump to comment: Most recent
Three findings from a security review before beta7, each confirmed in the code.
api/subscribe-url and api/portal-url are served as text/html. Both build their body with json_encode() and return a bare Response with no content type, which Symfony then labels HTML; json_encode() does not escape angle brackets. The subscribe-url payload carries each plan's name and price description, which arrive from the billing provider through syncPlans(). Anyone who can rename a product in the provider's dashboard, or a plan administrator, can put a script tag in a name; after the next sync, a subscriber who opens the URL (a same-origin link suffices) runs it in the site origin with their session. X-Content-Type-Options: nosniff does not help because the type is explicitly HTML. api/my-subscription already returns a JsonResponse.api/my-subscription returns the plan's raw prices map verbatim. The entity defines that field as "raw subscription plan prices data", whatever the connector stored from the remote service — provider metadata, lookup keys, internal notes. The serializer's own comment says raw remote payloads must not leak, but exempts this field, and MySubscriptionApiTest asserts that an arbitrary internal_note key round-trips to the subscriber. With #3629242: Let subscribers choose a plan, not a connector: list plans in the subscribe flow and preselect the plan in redirectToSubscribe() the module has a structured, connector-vetted PlanPrice; the API should return that.PostPurchaseTokenService::verify() checks whether a signature is burned, then writes the burn. Two presentations of a leaked auto-login token racing within the same instant both verify. The expirable key-value store offers setWithExpireIfNotExists(), which is atomic.getSubscribeUrl() and getPortalUrl() return a JsonResponse; the Kernel tests assert application/json on each, including the 503 shapes, and that a plan named with a script tag comes back escaped and typed as JSON.serializePlan() returns price — PlanPrice::toArray() from the plan's connector when it implements PlanPricingConnectorInterface, else null — and no longer returns prices. Breaking for front ends reading plan.prices; same shape as the chooser API's plans[].price. The test's internal_note assertion inverts: it must not appear.setWithExpireIfNotExists(), and verification fails when it returns FALSE; a test presents the same token twice and gets exactly one success.price field is the breaking part).None.
api/my-subscription's plan.prices is replaced by plan.price ({amount, currency, interval, interval_count, description} or null). api/subscribe-url and api/portal-url gain the application/json content type they should always have had. No connector interface changes.
None.
Comments
Comment #2
colan