VYPR
Low severityNVD Advisory· Published May 19, 2026· Updated May 19, 2026

CVE-2026-34154

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

Patches

1
a2061ae66d9f

FIX: Gate checkout provisioning on payment_status and handle async payments [backport 2026.1]

https://github.com/discourse/discourseDavid TaylorMay 18, 2026Fixed in 2026.1.4via llm-release-walk
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

1

News mentions

0

No linked articles in our index yet.