CVE-2026-34154
Description
Discourse is an open-source discussion platform. In versions prior to 2026.1.4, 2026.3.1, 2026.4.1 and 2026.5.0-latest.1, a vulnerability in the discourse-subscriptions plugin allows users to gain access to subscription-gated groups without completing payment. This issue has been fixed in versions 2026.1.4, 2026.3.1, 2026.4.1 and 2026.5.0-latest.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A vulnerability in Discourse's discourse-subscriptions plugin allows users to access subscription-gated groups without completing payment.
Vulnerability
The discourse-subscriptions plugin in Discourse versions prior to 2026.1.4, 2026.3.1, 2026.4.1, and 2026.5.0-latest.1 contains a flaw that permits users to gain access to subscription-gated groups without completing the required payment [1]. The vulnerability resides in the plugin's access control logic, which fails to properly verify payment status before granting group membership.
Exploitation
An attacker with a low-privileged account (e.g., a regular forum user) can exploit this vulnerability without any user interaction or special network position [1]. The attack complexity is low, meaning no advanced techniques are required. The attacker simply needs to request access to a subscription-gated group through the plugin's interface, and the payment check is bypassed, granting immediate access.
Impact
Successful exploitation allows the attacker to join subscription-gated groups without paying, gaining unauthorized access to restricted content, features, or privileges associated with those groups. This undermines the subscription model and could lead to information disclosure or privilege escalation within the community.
Mitigation
The issue is fixed in Discourse versions 2026.1.4, 2026.3.1, 2026.4.1, and 2026.5.0-latest.1 [1]. Users should upgrade to one of these versions or later. No workarounds are documented in the available reference. The vulnerability is not listed on the CISA Known Exploited Vulnerabilities (KEV) catalog as of the publication date.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2- Range: <2026.1.4
- Range: < 2026.1.4, < 2026.3.1, < 2026.4.1, < 2026.5.0-latest.1
Patches
1a2061ae66d9fFIX: Gate checkout provisioning on payment_status and handle async payments [backport 2026.1]
2 files changed · +72 −4
plugins/discourse-subscriptions/app/controllers/discourse_subscriptions/hooks_controller.rb+6 −4 modified@@ -27,15 +27,17 @@ def create end case event[:type] - when "checkout.session.completed" + when "checkout.session.completed", "checkout.session.async_payment_succeeded" checkout_session = event[:data][:object] + return head :ok if checkout_session[:status] != "complete" + return head :ok if checkout_session[:payment_status] != "paid" + if SiteSetting.discourse_subscriptions_enable_verbose_logging - Rails.logger.warn("checkout.session.completed data: #{checkout_session}") + Rails.logger.warn("#{event[:type]} data: #{checkout_session}") end - email = checkout_session[:customer_email] - return head :ok if checkout_session[:status] != "complete" + email = checkout_session[:customer_email] return render_json_error "email not found" if !email if checkout_session[:customer].nil?
plugins/discourse-subscriptions/spec/requests/hooks_controller_spec.rb+66 −0 modified@@ -191,6 +191,16 @@ expect(response.status).to eq 200 end end + + it "does not create records or add the user to the group when payment_status is not paid" do + unpaid_data = checkout_session_completed_data.deep_dup + unpaid_data[:object][:payment_status] = "unpaid" + event = { type: "checkout.session.completed", data: unpaid_data } + ::Stripe::Webhook.stubs(:construct_event).returns(event) + + expect { post "/s/hooks.json" }.not_to change { user.groups.count } + expect(response.status).to eq(200) + end end describe "checkout.session.completed with bad data" do @@ -369,5 +379,61 @@ expect(response.status).to eq 200 end end + + describe "checkout.session.async_payment_succeeded" do + before do + event = { + type: "checkout.session.async_payment_succeeded", + data: checkout_session_completed_data, + } + ::Stripe::Webhook.stubs(:construct_event).returns(event) + ::Stripe::Checkout::Session + .stubs(:list_line_items) + .with( + checkout_session_completed_data[:object][:id], + { limit: 1 }, + DiscourseSubscriptions::Stripe.request_opts, + ) + .returns(list_line_items_data) + ::Stripe::Subscription + .stubs(:update) + .with( + checkout_session_completed_data[:object][:subscription], + { metadata: { user_id: user.id, username: user.username } }, + DiscourseSubscriptions::Stripe.request_opts, + ) + .returns( + { + id: checkout_session_completed_data[:object][:subscription], + object: "subscription", + metadata: { + user_id: user.id.to_s, + username: user.username, + }, + }, + ) + end + + it "creates customer and subscription records and adds the user to the group" do + post "/s/hooks.json" + + expect(response.status).to eq(200) + expect(user.groups).to include(group) + + expect( + DiscourseSubscriptions::Customer.exists?( + user_id: user.id, + customer_id: customer.customer_id, + product_id: "prod_PhB6IpGhEX14Hi", + ), + ).to eq(true) + expect( + DiscourseSubscriptions::Subscription.exists?( + customer_id: DiscourseSubscriptions::Customer.last.id, + external_id: "sub_1P9b7iEYXaQnncSh3H3G9d2Y", + ), + ).to eq(true) + end + end end end
Vulnerability mechanics
Root cause
"Missing authorization check: the plugin provisions subscription-gated group membership based solely on Stripe checkout session completion, without verifying that the session's payment_status is "paid"."
Attack vector
An attacker initiates a Stripe Checkout session using a delayed payment method (e.g., bank transfer, SEPA debit) that triggers `checkout.session.completed` with `payment_status: "unpaid"`. The plugin's webhook handler [patch_id=898945] previously accepted any completed session regardless of payment status, provisioning customer/subscription records and adding the user to the gated group. No authentication is required beyond having a valid Discourse account; the attacker simply completes a checkout flow that does not require immediate payment.
Affected code
The vulnerability resides in `plugins/discourse-subscriptions/app/controllers/discourse_subscriptions/hooks_controller.rb` [patch_id=898945]. The `create` action handles Stripe webhook events and previously provisioned group membership for any `checkout.session.completed` event without inspecting the `payment_status` field. The spec file `plugins/discourse-subscriptions/spec/requests/hooks_controller_spec.rb` was updated to verify the new behavior.
What the fix does
The patch adds two guard clauses in the webhook handler [patch_id=898945]: `return head :ok if checkout_session[:status] != "complete"` (pre-existing) and the new `return head :ok if checkout_session[:payment_status] != "paid"`. This ensures that sessions with `payment_status` of `"unpaid"` are silently acknowledged but never provisioned. A new handler for `checkout.session.async_payment_succeeded` is also introduced, which provisions access only when Stripe confirms the delayed payment has actually succeeded, closing the window where a user could gain group access without paying.
Preconditions
- authAttacker must have a valid Discourse account on the target instance.
- configThe Discourse instance must have the discourse-subscriptions plugin enabled with subscription-gated groups configured.
- inputAttacker must initiate a Stripe Checkout session using a delayed payment method (e.g., bank transfer, SEPA debit) that completes with payment_status 'unpaid'.
Generated on May 20, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
1News mentions
0No linked articles in our index yet.