CVE-2024-27090
Description
Decidim is a participatory democracy framework, written in Ruby on Rails, originally developed for the Barcelona City government online and offline participation website. If an attacker can infer the slug or URL of an unpublished or private resource, and this resource can be embbeded (such as a Participatory Process, an Assembly, a Proposal, a Result, etc), then some data of this resource could be accessed. This vulnerability is fixed in 0.27.6.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Decidim exposes unpublished or private resource data to attackers who can guess the resource slug or URL, fixed in version 0.27.6.
Vulnerability
Overview
CVE-2024-27090 is an information exposure vulnerability in Decidim, a Ruby on Rails participatory democracy framework [2]. The issue arises because unpublished or private resources—such as participatory processes, assemblies, proposals, or results—can still be embedded and accessed if an attacker correctly infers the resource's slug or URL. No authentication or special privileges are required to trigger the exposure, only knowledge of the resource identifier [1].
Exploitation
Method
An attacker who can guess or enumerate the slug or URL of an unpublished or private resource can access it via embedding. This is feasible because many Decidim deployments use predictable URL patterns, making it possible to discover hidden resources without authorization. The vulnerability does not require the attacker to be logged in or to have any special network position [3].
Impact
Successful exploitation allows the attacker to view data from otherwise restricted resources. This includes potentially sensitive information such as the titles, descriptions, and other metadata of draft or private participation components. The exposure is limited to what the resource's public embedding would show, but it still represents a breach of the intended access controls [1].
Mitigation
Decidim addressed the vulnerability in version 0.27.6. All installations should upgrade to this or a later release to prevent unauthorized access to unpublished or private content. No workarounds have been provided, and the vendor advises upgrading promptly [1][3].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
decidimRubyGems | < 0.27.6 | 0.27.6 |
Affected products
2Patches
21756fa639ef3Fix embeds for resources and spaces that shouldn't be embedded (#12528)
44 files changed · +799 −57
decidim-assemblies/app/controllers/decidim/assemblies/widgets_controller.rb+11 −1 modified@@ -5,10 +5,16 @@ module Assemblies class WidgetsController < Decidim::WidgetsController helper Decidim::SanitizeHelper + def show + enforce_permission_to :embed, :participatory_space, current_participatory_space: model if model + + super + end + private def model - @model ||= Assembly.find_by(slug: params[:assembly_slug]) + @model ||= Assembly.where(organization: current_organization).public_spaces.find_by(slug: params[:assembly_slug]) end def current_participatory_space @@ -18,6 +24,10 @@ def current_participatory_space def iframe_url @iframe_url ||= assembly_widget_url(model) end + + def permission_class_chain + ::Decidim.permissions_registry.chain_for(::Decidim::Assemblies::ApplicationController) + end end end end
decidim-assemblies/app/permissions/decidim/assemblies/permissions.rb+12 −0 modified@@ -16,6 +16,7 @@ def permissions if permission_action.scope == :public public_list_assemblies_action? public_read_assembly_action? + public_embed_assembly_action? public_list_members_action? return permission_action end @@ -136,6 +137,17 @@ def public_list_members_action? allow! end + def public_embed_assembly_action? + return unless permission_action.action == :embed && + [:assembly, :participatory_space].include?(permission_action.subject) && + assembly + + return disallow! unless assembly.published? + return disallow! if assembly.private_space && !assembly.is_transparent? + + allow! + end + # All users with a relation to a assembly and organization admins can enter # the space area. The sapce area is considered to be the assemblies zone, # not the assembly groups one.
decidim-assemblies/spec/permissions/decidim/assemblies/permissions_spec.rb+30 −0 modified@@ -127,6 +127,36 @@ end end + context "when embedding an assembly" do + let(:action) do + { scope: :public, action: :embed, subject: :assembly } + end + let(:context) { { assembly: assembly } } + + context "when the assembly is published" do + let(:user) { create(:user, organization: organization) } + + it { is_expected.to be true } + end + + context "when the assembly is not published" do + let(:user) { create(:user, organization: organization) } + let(:assembly) { create(:assembly, :unpublished, organization: organization) } + + context "when the user doesn't have access to it" do + it { is_expected.to be false } + end + + context "when the user has access to it" do + before do + create(:assembly_user_role, user: user, assembly: assembly) + end + + it { is_expected.to be false } + end + end + end + context "when listing assemblies" do let(:action) do { scope: :public, action: :list, subject: :assembly }
decidim-assemblies/spec/system/assembly_embeds_spec.rb+3 −0 modified@@ -4,6 +4,9 @@ describe "Assembly embeds", type: :system do let(:resource) { create(:assembly) } + let(:widget_path) { Decidim::EngineRouter.main_proxy(resource).assembly_widget_path } it_behaves_like "an embed resource", skip_space_checks: true + it_behaves_like "a private embed resource" + it_behaves_like "a transparent private embed resource" end
decidim-conferences/app/controllers/decidim/conferences/conference_widgets_controller.rb+11 −1 modified@@ -5,10 +5,16 @@ module Conferences class ConferenceWidgetsController < Decidim::WidgetsController helper Decidim::SanitizeHelper + def show + enforce_permission_to :embed, :conference, conference: model if model + + super + end + private def model - @model ||= Conference.find_by(slug: params[:conference_slug]) + @model ||= Conference.where(organization: current_organization).published.find_by(slug: params[:conference_slug]) end def current_participatory_space @@ -18,6 +24,10 @@ def current_participatory_space def iframe_url @iframe_url ||= conference_conference_widget_url(model) end + + def permission_class_chain + ::Decidim.permissions_registry.chain_for(::Decidim::Conferences::ApplicationController) + end end end end
decidim-conferences/app/permissions/decidim/conferences/permissions.rb+11 −0 modified@@ -16,6 +16,7 @@ def permissions if permission_action.scope == :public public_list_conferences_action? public_read_conference_action? + public_embed_conference_action? public_list_speakers_action? public_list_program_action? public_list_media_links_action? @@ -131,6 +132,16 @@ def public_read_conference_action? toggle_allow(can_manage_conference?) end + def public_embed_conference_action? + return unless permission_action.action == :embed && + [:conference, :participatory_space].include?(permission_action.subject) && + conference + + return disallow! unless conference.published? + + allow! + end + def public_list_speakers_action? return unless permission_action.action == :list && permission_action.subject == :speakers
decidim-conferences/spec/permissions/decidim/conferences/permissions_spec.rb+36 −0 modified@@ -125,6 +125,42 @@ end end + context "when embedding a conference" do + let(:action) do + { scope: :public, action: :embed, subject: :conference } + end + let(:context) { { conference: conference } } + + context "when the user is an admin" do + let(:user) { create :user, :admin } + + it { is_expected.to be true } + end + + context "when the conference is published" do + let(:user) { create :user, organization: organization } + + it { is_expected.to be true } + end + + context "when the conference is not published" do + let(:user) { create :user, organization: organization } + let(:conference) { create :conference, :unpublished, organization: organization } + + context "when the user doesn't have access to it" do + it { is_expected.to be false } + end + + context "when the user has access to it" do + before do + create :conference_user_role, user: user, conference: conference + end + + it { is_expected.to be false } + end + end + end + context "when listing conferences" do let(:action) do { scope: :public, action: :list, subject: :conference }
decidim-conferences/spec/system/conference_embeds_spec.rb+3 −13 modified@@ -3,18 +3,8 @@ require "spec_helper" describe "Conference embeds", type: :system do - let!(:conference) { create(:conference) } + let!(:resource) { create(:conference) } + let(:widget_path) { Decidim::EngineRouter.main_proxy(resource).conference_conference_widget_path } - context "when visiting the embed page for an conference" do - before do - switch_to_host(conference.organization.host) - visit resource_locator(conference).path - visit "#{current_path}/embed" - end - - it "renders the page correctly" do - expect(page).to have_i18n_content(conference.title) - expect(page).to have_content(conference.organization.name) - end - end + it_behaves_like "an embed resource", skip_space_checks: true, skip_link_checks: true end
decidim-consultations/app/controllers/decidim/consultations/consultation_widgets_controller.rb+12 −1 modified@@ -4,13 +4,20 @@ module Decidim module Consultations class ConsultationWidgetsController < Decidim::WidgetsController helper Decidim::SanitizeHelper + helper ConsultationsHelper layout false + def show + enforce_permission_to :embed, :participatory_space, current_participatory_space: model if model + + super + end + private def model - @model ||= Consultation.find_by(slug: params[:consultation_slug]) + @model ||= Consultation.where(organization: current_organization).published.find_by(slug: params[:consultation_slug]) end def current_participatory_space @@ -20,6 +27,10 @@ def current_participatory_space def iframe_url @iframe_url ||= consultation_consultation_widget_url(model) end + + def permission_class_chain + ::Decidim.permissions_registry.chain_for(::Decidim::Consultations::ApplicationController) + end end end end
decidim-consultations/app/controllers/decidim/consultations/question_widgets_controller.rb+11 −1 modified@@ -8,10 +8,16 @@ class QuestionWidgetsController < Decidim::WidgetsController helper Decidim::SanitizeHelper + def show + enforce_permission_to :embed, :question, question: model if model + + super + end + private def model - @model ||= current_question + @model ||= current_question if current_question.published? end def current_participatory_space @@ -21,6 +27,10 @@ def current_participatory_space def iframe_url @iframe_url ||= question_question_widget_url(model) end + + def permission_class_chain + ::Decidim.permissions_registry.chain_for(::Decidim::Consultations::ApplicationController) + end end end end
decidim-consultations/app/permissions/decidim/consultations/permissions.rb+21 −1 modified@@ -5,6 +5,8 @@ module Consultations class Permissions < Decidim::DefaultPermissions def permissions allowed_public_anonymous_action? + allowed_public_embed_consultation_action? + allowed_public_embed_question_action? return permission_action unless user @@ -22,7 +24,7 @@ def question end def consultation - @consultation ||= context.fetch(:consultation, nil) + @consultation ||= context.fetch(:current_participatory_space, nil) || context.fetch(:consultation, nil) end def authorized?(permission_action, resource: nil) @@ -45,6 +47,24 @@ def allowed_public_anonymous_action? end end + def allowed_public_embed_consultation_action? + return unless permission_action.action == :embed && + [:consultation, :participatory_space].include?(permission_action.subject) && + consultation + + return disallow! unless consultation.published? + + allow! + end + + def allowed_public_embed_question_action? + return unless permission_action.action == :embed && permission_action.subject == :question && question + + return disallow! unless question.published? + + allow! + end + def allowed_public_action? return unless permission_action.scope == :public return unless permission_action.subject == :question
decidim-consultations/app/views/decidim/consultations/consultation_widgets/show.html.erb+3 −0 modified@@ -1,3 +1,6 @@ +<p><%= translated_attribute(model.title) %></p> +<p><%= current_organization.name %></p> + <%= render partial: "decidim/consultations/consultations/consultation_details", locals: { consultation: model } %> <%= render partial: "decidim/consultations/consultations/highlighted_questions", locals: { consultation: model } %> <%= render partial: "decidim/consultations/consultations/regular_questions", locals: { consultation: model } %>
decidim-consultations/spec/permissions/decidim/consultations/permissions_spec.rb+54 −0 modified@@ -52,6 +52,33 @@ end end + context "when embedding a consultation" do + let(:action_name) { :embed } + let(:action_subject) { :consultation } + + context "when the consultation is published" do + let(:consultation) { create :consultation, :published } + + it { is_expected.to be true } + end + + context "when the consultation is not published" do + let(:consultation) { create :consultation, :unpublished } + + context "when the user is not an admin" do + let(:user) { nil } + + it { is_expected.to be false } + end + + context "when the user is an admin" do + let(:user) { create :user, :admin, organization: organization } + + it { is_expected.to be false } + end + end + end + context "when reading a question" do let(:action_subject) { :question } @@ -78,6 +105,33 @@ end end + context "when embedding a question" do + let(:action_name) { :embed } + let(:action_subject) { :question } + + context "when the question is published" do + let(:question) { create :question, :published, consultation: consultation } + + it { is_expected.to be true } + end + + context "when the question is not published" do + let(:question) { create :question, :unpublished, consultation: consultation } + + context "when the user is not an admin" do + let(:user) { nil } + + it { is_expected.to be false } + end + + context "when the user is an admin" do + let(:user) { create :user, :admin, organization: organization } + + it { is_expected.to be false } + end + end + end + context "when voting a question" do let(:action_subject) { :question } let(:action_name) { :vote }
decidim-consultations/spec/system/consultation_embeds_spec.rb+10 −0 added@@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Consultation embeds", type: :system do + let(:resource) { create(:consultation) } + let(:widget_path) { Decidim::EngineRouter.main_proxy(resource).consultation_consultation_widget_path } + + it_behaves_like "an embed resource", skip_space_checks: true, skip_link_checks: true +end
decidim-consultations/spec/system/question_embeds_spec.rb+3 −12 modified@@ -3,17 +3,8 @@ require "spec_helper" describe "Question embeds", type: :system do - let(:question) { create(:question) } + let(:resource) { create(:question) } + let(:widget_path) { Decidim::EngineRouter.main_proxy(resource).question_question_widget_path } - context "when visiting the embed page for a question" do - before do - switch_to_host(question.organization.host) - visit "#{decidim_consultations.question_path(question)}/embed" - end - - it "renders the page correctly" do - expect(page).to have_i18n_content(question.title) - expect(page).to have_content(question.organization.name) - end - end + it_behaves_like "an embed resource", skip_space_checks: true, skip_link_checks: true end
decidim-core/app/controllers/decidim/widgets_controller.rb+6 −0 modified@@ -11,6 +11,8 @@ class WidgetsController < Decidim::ApplicationController helper_method :model, :iframe_url, :current_participatory_space def show + raise ActionController::RoutingError, "Not Found" if model.nil? + respond_to do |format| format.js { render "decidim/widgets/show" } format.html @@ -19,6 +21,10 @@ def show private + def current_component + @current_component ||= request.env["decidim.current_component"] + end + def current_participatory_space @current_participatory_space ||= model.component.participatory_space end
decidim-core/lib/decidim/core/test/shared_examples/embed_resource_examples.rb+187 −11 modified@@ -1,5 +1,50 @@ # frozen_string_literal: true +require "decidim/admin/test/admin_participatory_space_access_examples" + +shared_examples "rendering the embed page correctly" do + before do + visit widget_path + end + + it "renders" do + if resource.title.is_a?(Hash) + expect(page).to have_i18n_content(resource.title) + else + expect(page).to have_content(resource.title) + end + + expect(page).to have_content(organization.name) + end +end + +shared_examples "rendering the embed link in the resource page" do + before do + visit resource_locator(resource).path + end + + it "has the embed link" do + expect(page).to have_button("Embed") + end +end + +shared_examples "showing the unauthorized message in the widget_path" do + it do + visit widget_path + expect(page).to have_content "You are not authorized to perform this action" + end +end + +shared_examples "not rendering the embed link in the resource page" do + before do + visit resource_locator(resource).path + end + + it "does not have the embed link" do + expect(page).to have_no_button("Embed") + end +end + shared_examples_for "an embed resource" do |options| if options.is_a?(Hash) && options[:skip_space_checks] let(:organization) { resource.organization } @@ -11,22 +56,29 @@ include_context "with a component" end - context "when visiting the embed page for a resource" do - before do - visit resource_locator(resource).path - visit "#{current_path}/embed" - end + unless options.is_a?(Hash) && options[:skip_publication_checks] + context "when the resource is not published" do + before do + resource.unpublish! + end + + it_behaves_like "not rendering the embed link in the resource page" - it "renders the page correctly" do - if resource.title.is_a?(Hash) - expect(page).to have_i18n_content(resource.title) - else - expect(page).to have_content(resource.title) + it_behaves_like "a 404 page" do + let(:target_path) { widget_path } end + end + end + + it_behaves_like "rendering the embed link in the resource page" unless options.is_a?(Hash) && options[:skip_link_checks] - expect(page).to have_content(organization.name) + context "when visiting the embed page for a resource" do + before do + visit widget_path end + it_behaves_like "rendering the embed page correctly" + unless options.is_a?(Hash) && options[:skip_space_checks] context "when the participatory_space is a process" do it "shows the process name" do @@ -47,3 +99,127 @@ end end end + +shared_examples_for "a private embed resource" do + let(:organization) { resource.organization } + let!(:other_user) { create(:user, :confirmed, organization: organization) } + let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: other_user, privatable_to: resource) } + + before do + switch_to_host(organization.host) + end + + context "when the resource is private" do + before do + resource.update!(private_space: true) + resource.update!(is_transparent: false) if resource.respond_to?(:is_transparent) + end + + context "and user is a visitor" do + let(:user) { nil } + + it_behaves_like "not rendering the embed link in the resource page" + + it_behaves_like "a 404 page" do + let(:target_path) { widget_path } + end + end + + context "and user is a registered user" do + let(:user) { create(:user, :confirmed, organization: organization) } + + before do + sign_in user, scope: :user + end + + it_behaves_like "not rendering the embed link in the resource page" + + it_behaves_like "a 404 page" do + let(:target_path) { widget_path } + end + end + + context "and user is a private user" do + let(:user) { other_user } + + before do + sign_in user, scope: :user + end + + it_behaves_like "a 404 page" do + let(:target_path) { widget_path } + end + end + end +end + +shared_examples_for "a transparent private embed resource" do + let(:organization) { resource.organization } + let!(:other_user) { create(:user, :confirmed, organization: organization) } + let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: other_user, privatable_to: resource) } + + before do + switch_to_host(organization.host) + end + + context "when the resource is private" do + before do + resource.update!(private_space: true) + resource.update!(is_transparent: true) if resource.respond_to?(:is_transparent) + end + + context "and user is a visitor" do + let(:user) { nil } + + it_behaves_like "rendering the embed page correctly" + end + + context "and user is a registered user" do + let(:user) { create(:user, :confirmed, organization: organization) } + + before do + sign_in user, scope: :user + end + + it_behaves_like "rendering the embed page correctly" + end + + context "and user is a private user" do + let(:user) { other_user } + + before do + sign_in user, scope: :user + end + + it_behaves_like "rendering the embed page correctly" + end + end +end + +shared_examples_for "a moderated embed resource" do + include_context "with a component" + + context "when the resource is moderated" do + let!(:moderation) { create(:moderation, reportable: resource, hidden_at: 2.days.ago) } + + it_behaves_like "a 404 page" do + let(:target_path) { widget_path } + end + end +end + +shared_examples_for "a withdrawn embed resource" do + include_context "with a component" + + context "when the resource is withdrawn" do + before do + resource.update!(state: "withdrawn") + end + + it_behaves_like "not rendering the embed link in the resource page" + + it_behaves_like "a 404 page" do + let(:target_path) { widget_path } + end + end +end
decidim-debates/app/controllers/decidim/debates/widgets_controller.rb+11 −1 modified@@ -5,15 +5,25 @@ module Debates class WidgetsController < Decidim::WidgetsController helper Debates::ApplicationHelper + def show + enforce_permission_to :embed, :debate, debate: model if model + + super + end + private def model - @model ||= Debate.where(component: params[:component_id]).find(params[:debate_id]) + @model ||= Debate.not_hidden.where(component: current_component).find(params[:debate_id]) end def iframe_url @iframe_url ||= debate_widget_url(model) end + + def permission_class_chain + [Decidim::Debates::Permissions] + end end end end
decidim-debates/app/permissions/decidim/debates/permissions.rb+6 −0 modified@@ -21,6 +21,8 @@ def permissions can_endorse_debate? when :close can_close_debate? + when :embed + can_embed_debate? end permission_action @@ -45,6 +47,10 @@ def can_close_debate? disallow! end + def can_embed_debate? + allow! + end + def can_endorse_debate? return disallow! if debate.closed?
decidim-debates/spec/permissions/decidim/debates/permissions_spec.rb+14 −0 modified@@ -106,4 +106,18 @@ it { is_expected.to be false } end end + + context "when embedding a debate" do + let(:action) do + { scope: :public, action: :embed, subject: :debate } + end + + it { is_expected.to be true } + + context "when the debate is closed" do + let(:debate) { create :debate, :closed, component: debates_component } + + it { is_expected.to be true } + end + end end
decidim-debates/spec/system/debate_embeds_spec.rb+4 −1 modified@@ -4,8 +4,11 @@ describe "Debate embeds", type: :system do include_context "with a component" + let(:manifest_name) { "debates" } let!(:resource) { create(:debate, component: component, skip_injection: true) } + let(:widget_path) { Decidim::EngineRouter.main_proxy(component).debate_widget_path(resource) } - it_behaves_like "an embed resource" + it_behaves_like "an embed resource", skip_publication_checks: true + it_behaves_like "a moderated embed resource" end
decidim-initiatives/app/controllers/decidim/initiatives/widgets_controller.rb+15 −1 modified@@ -12,10 +12,20 @@ class WidgetsController < Decidim::WidgetsController include NeedsInitiative + def show + enforce_permission_to :embed, :participatory_space, current_participatory_space: model if model + + super + end + private def model - @model ||= current_initiative + @model ||= if current_initiative.created? || current_initiative.validating? || current_initiative.discarded? + nil + else + current_initiative + end end def current_participatory_space @@ -25,6 +35,10 @@ def current_participatory_space def iframe_url @iframe_url ||= initiative_widget_url(model) end + + def permission_class_chain + ::Decidim.permissions_registry.chain_for(::Decidim::Initiatives::ApplicationController) + end end end end
decidim-initiatives/app/permissions/decidim/initiatives/permissions.rb+10 −0 modified@@ -11,6 +11,7 @@ def permissions # Non-logged users permissions list_public_initiatives? read_public_initiative? + embed_public_initiative? search_initiative_types_and_scopes? request_membership? @@ -57,6 +58,15 @@ def read_public_initiative? disallow! end + def embed_public_initiative? + return unless [:initiative, :participatory_space].include?(permission_action.subject) && + permission_action.action == :embed + + return disallow! if initiative.created? || initiative.validating? || initiative.discarded? + + allow! + end + def search_initiative_types_and_scopes? return unless permission_action.action == :search return unless [:initiative_type, :initiative_type_scope, :initiative_type_signature_types].include?(permission_action.subject)
decidim-initiatives/app/views/decidim/initiatives/initiatives/show.html.erb+3 −1 modified@@ -65,7 +65,9 @@ edit_link( </div> <% end %> <%= render partial: "decidim/shared/share_modal" %> - <%= embed_modal_for initiative_widget_url(current_initiative, format: :js) %> + <% if allowed_to? :embed, :initiative, initiative: current_initiative %> + <%= embed_modal_for initiative_widget_url(current_initiative, format: :js) %> + <% end %> <%= resource_reference(current_initiative) %> <%= resource_version(current_initiative, versions_path: initiative_versions_path(current_initiative)) %> </div>
decidim-initiatives/spec/permissions/decidim/initiatives/permissions_spec.rb+70 −0 modified@@ -149,6 +149,76 @@ end end + context "when emeding an initiative" do + let(:initiative) { create(:initiative, :accepted, organization: organization) } + let(:action) do + { scope: :public, action: :embed, subject: :initiative } + end + let(:context) do + { initiative: initiative } + end + + context "when initiative is created" do + let(:initiative) { create(:initiative, :created, organization: organization) } + + it { is_expected.to be false } + end + + context "when initiative is validating" do + let(:initiative) { create(:initiative, :validating, organization: organization) } + + it { is_expected.to be false } + end + + context "when initiative is discarded" do + let(:initiative) { create(:initiative, :discarded, organization: organization) } + + it { is_expected.to be false } + end + + context "when initiative is published" do + let(:initiative) { create(:initiative, :published, organization: organization) } + + it { is_expected.to be true } + end + + context "when initiative is rejected" do + let(:initiative) { create(:initiative, :rejected, organization: organization) } + + it { is_expected.to be true } + end + + context "when initiative is accepted" do + let(:initiative) { create(:initiative, :accepted, organization: organization) } + + it { is_expected.to be true } + end + + context "when user is admin" do + let(:user) { create(:user, :admin, organization: organization) } + + it { is_expected.to be true } + end + + context "when user is author of the initiative" do + let(:initiative) { create(:initiative, author: user, organization: organization) } + + it { is_expected.to be true } + end + + context "when user is committee member of the initiative" do + before do + create(:initiatives_committee_member, initiative: initiative, user: user) + end + + it { is_expected.to be true } + end + + context "when any other condition" do + it { is_expected.to be true } + end + end + context "when listing committee members of the initiative as author" do let(:initiative) { create(:initiative, organization: organization, author: user) } let(:action) do
decidim-initiatives/spec/system/initiative_embeds_spec.rb+64 −2 modified@@ -3,7 +3,69 @@ require "spec_helper" describe "Initiative embeds", type: :system do - let(:resource) { create(:initiative) } + let(:state) { :published } + let(:resource) { create(:initiative, state: state) } + let(:widget_path) { Decidim::EngineRouter.main_proxy(resource).initiative_widget_path } - it_behaves_like "an embed resource", skip_space_checks: true + it_behaves_like "an embed resource", skip_space_checks: true, skip_publication_checks: true + + context "when the user is the initiative author" do + let(:organization) { resource.organization } + let(:user) { resource.author } + + before do + switch_to_host(organization.host) + end + + context "when the state is created" do + let(:state) { :created } + + it_behaves_like "not rendering the embed link in the resource page" + + it_behaves_like "a 404 page" do + let(:target_path) { widget_path } + end + end + + context "when the state is validating" do + let(:state) { :validating } + + it_behaves_like "not rendering the embed link in the resource page" + + it_behaves_like "a 404 page" do + let(:target_path) { widget_path } + end + end + + context "when the state is discarded" do + let(:state) { :discarded } + + # A discarded initiative is not available anymore to authors + + it_behaves_like "a 404 page" do + let(:target_path) { widget_path } + end + end + + context "when the state is published" do + let(:state) { :published } + + it_behaves_like "rendering the embed link in the resource page" + it_behaves_like "rendering the embed page correctly" + end + + context "when the state is rejected" do + let(:state) { :rejected } + + it_behaves_like "rendering the embed link in the resource page" + it_behaves_like "rendering the embed page correctly" + end + + context "when the state is accepted" do + let(:state) { :accepted } + + it_behaves_like "rendering the embed link in the resource page" + it_behaves_like "rendering the embed page correctly" + end + end end
decidim-meetings/app/controllers/decidim/meetings/widgets_controller.rb+11 −1 modified@@ -6,15 +6,25 @@ class WidgetsController < Decidim::WidgetsController helper MeetingsHelper helper Decidim::SanitizeHelper + def show + enforce_permission_to :embed, :meeting, meeting: model if model + + super + end + private def model - @model ||= Meeting.where(component: params[:component_id]).find(params[:meeting_id]) + @model ||= Meeting.except_withdrawn.published.not_hidden.where(component: current_component).find(params[:meeting_id]) end def iframe_url @iframe_url ||= meeting_widget_url(model) end + + def permission_class_chain + [Decidim::Meetings::Permissions] + end end end end
decidim-meetings/app/permissions/decidim/meetings/permissions.rb+10 −0 modified@@ -4,6 +4,7 @@ module Decidim module Meetings class Permissions < Decidim::DefaultPermissions def permissions + allow_embed_meeting? return permission_action unless user # Delegate the admin permission checks to the admin permissions class @@ -57,6 +58,15 @@ def question @question ||= context.fetch(:question, nil) end + # As this is a public action, we need to run this before other checks + def allow_embed_meeting? + return unless permission_action.action == :embed && permission_action.subject == :meeting && meeting + return disallow! if meeting.withdrawn? + return allow! if meeting.published? + + disallow! + end + def can_join_meeting? meeting.can_be_joined_by?(user) && authorized?(:join, resource: meeting)
decidim-meetings/app/views/decidim/meetings/meetings/show.html.erb+3 −1 modified@@ -105,7 +105,9 @@ edit_link( <%= resource_version(meeting, versions_path: meeting_versions_path(meeting)) %> <%= cell "decidim/meetings/cancel_registration_meeting_button", meeting %> <%= render partial: "decidim/shared/share_modal" %> - <%= embed_modal_for meeting_widget_url(meeting, format: :js) %> + <% if allowed_to? :embed, :meeting, meeting: @meeting %> + <%= embed_modal_for meeting_widget_url(meeting, format: :js) %> + <% end %> <%= render partial: "calendar_modal", locals: { ics_url: calendar_meeting_url(meeting), google_url: google_calendar_event_url(meeting) } %> </div> <div class="columns mediumlarge-8 mediumlarge-pull-4">
decidim-meetings/spec/permissions/decidim/meetings/permissions_spec.rb+22 −0 modified@@ -82,6 +82,28 @@ end end + context "when embedding a meeting" do + let(:action) do + { scope: :public, action: :embed, subject: :meeting } + end + + context "when meeting isn't published" do + before do + allow(meeting).to receive(:published?).and_return(false) + end + + it { is_expected.to be false } + end + + context "when meeting is published" do + before do + allow(meeting).to receive(:published?).and_return(true) + end + + it { is_expected.to be true } + end + end + context "when joining a meeting" do let(:action) do { scope: :public, action: :join, subject: :meeting }
decidim-meetings/spec/system/meeting_embeds_spec.rb+4 −1 modified@@ -4,9 +4,12 @@ describe "Meeting embeds", type: :system do include_context "with a component" - let(:manifest_name) { "meetings" } + let(:manifest_name) { "meetings" } let!(:resource) { create(:meeting, :published, component: component) } + let(:widget_path) { Decidim::EngineRouter.main_proxy(component).meeting_widget_path(resource) } it_behaves_like "an embed resource" + it_behaves_like "a moderated embed resource" + it_behaves_like "a withdrawn embed resource" end
decidim-participatory_processes/app/controllers/decidim/participatory_processes/widgets_controller.rb+12 −2 modified@@ -5,13 +5,19 @@ module ParticipatoryProcesses class WidgetsController < Decidim::WidgetsController helper Decidim::SanitizeHelper + def show + enforce_permission_to :embed, :participatory_space, current_participatory_space: model + + super + end + private def model return unless params[:participatory_process_slug] - @model ||= ParticipatoryProcess.where(slug: params[:participatory_process_slug]).or( - ParticipatoryProcess.where(id: params[:participatory_process_slug]) + @model ||= ParticipatoryProcess.where(organization: current_organization).public_spaces.where(slug: params[:participatory_process_slug]).or( + ParticipatoryProcess.where(organization: current_organization).public_spaces.where(id: params[:participatory_process_slug]) ).first! end @@ -22,6 +28,10 @@ def current_participatory_space def iframe_url @iframe_url ||= participatory_process_widget_url(model) end + + def permission_class_chain + ::Decidim.permissions_registry.chain_for(::Decidim::ParticipatoryProcesses::ApplicationController) + end end end end
decidim-participatory_processes/app/permissions/decidim/participatory_processes/permissions.rb+12 −0 modified@@ -19,6 +19,7 @@ def permissions public_list_process_groups_action? public_read_process_group_action? public_read_process_action? + public_embed_process_action? return permission_action end @@ -112,6 +113,17 @@ def public_read_process_action? toggle_allow(can_manage_process?) end + def public_embed_process_action? + return unless permission_action.action == :embed && + [:process, :participatory_space].include?(permission_action.subject) && + process + + return disallow! unless process.published? + return disallow! if process.private_space + + allow! + end + def can_view_private_space? return true unless process.private_space return false unless user
decidim-participatory_processes/spec/permissions/decidim/participatory_processes/permissions_spec.rb+30 −0 modified@@ -135,6 +135,36 @@ end end + context "when embedding an process" do + let(:action) do + { scope: :public, action: :embed, subject: :process } + end + let(:context) { { process: process } } + + context "when the process is published" do + let(:user) { create(:user, organization: organization) } + + it { is_expected.to be true } + end + + context "when the process is not published" do + let(:user) { create(:user, organization: organization) } + let(:process) { create(:participatory_process, :unpublished, organization: organization) } + + context "when the user doesn't have access to it" do + it { is_expected.to be false } + end + + context "when the user has access to it" do + before do + create(:participatory_process_user_role, user: user, participatory_process: process) + end + + it { is_expected.to be false } + end + end + end + context "when listing processes" do let(:action) do { scope: :public, action: :list, subject: :process }
decidim-participatory_processes/spec/system/process_embeds_spec.rb+2 −0 modified@@ -4,6 +4,8 @@ describe "Process embeds", type: :system do let(:resource) { create(:participatory_process) } + let(:widget_path) { Decidim::EngineRouter.main_proxy(resource).participatory_process_widget_path } it_behaves_like "an embed resource", skip_space_checks: true + it_behaves_like "a private embed resource" end
decidim-proposals/app/controllers/decidim/proposals/widgets_controller.rb+11 −1 modified@@ -5,15 +5,25 @@ module Proposals class WidgetsController < Decidim::WidgetsController helper Proposals::ApplicationHelper + def show + enforce_permission_to :embed, :proposal, proposal: model if model + + super + end + private def model - @model ||= Proposal.where(component: params[:component_id]).find(params[:proposal_id]) + @model ||= Proposal.not_hidden.except_withdrawn.where(component: current_component).find(params[:proposal_id]) end def iframe_url @iframe_url ||= proposal_widget_url(model) end + + def permission_class_chain + [Decidim::Proposals::Permissions] + end end end end
decidim-proposals/app/permissions/decidim/proposals/permissions.rb+9 −0 modified@@ -4,6 +4,7 @@ module Decidim module Proposals class Permissions < Decidim::DefaultPermissions def permissions + allow_embed_proposal? return permission_action unless user # Delegate the admin permission checks to the admin permissions class @@ -47,6 +48,14 @@ def proposal @proposal ||= context.fetch(:proposal, nil) || context.fetch(:resource, nil) end + # As this is a public action, we need to run this before other checks + def allow_embed_proposal? + return unless permission_action.action == :embed && permission_action.subject == :proposal && proposal + return disallow! if proposal.withdrawn? + + allow! + end + def voting_enabled? return unless current_settings
decidim-proposals/app/views/decidim/proposals/proposals/show.html.erb+3 −1 modified@@ -129,7 +129,9 @@ extra_admin_link( <%= resource_version(proposal_presenter, versions_path: proposal_versions_path(@proposal)) %> <%= cell("decidim/fingerprint", @proposal) %> <%= render partial: "decidim/shared/share_modal", locals: { resource: @proposal } %> - <%= embed_modal_for proposal_widget_url(@proposal, format: :js) %> + <% if allowed_to? :embed, :proposal, proposal: @proposal %> + <%= embed_modal_for proposal_widget_url(@proposal, format: :js) %> + <% end %> <%= cell "decidim/proposals/proposal_link_to_collaborative_draft", @proposal %> <%= cell "decidim/proposals/proposal_link_to_rejected_emendation", @proposal %> </div>
decidim-proposals/spec/permissions/decidim/proposals/permissions_spec.rb+8 −0 modified@@ -110,6 +110,14 @@ end end + context "when emebeding a proposal" do + let(:action) do + { scope: :public, action: :embed, subject: :proposal } + end + + it { is_expected.to be true } + end + describe "voting" do let(:action) do { scope: :public, action: :vote, subject: :proposal }
decidim-proposals/spec/system/proposal_embeds_spec.rb+5 −1 modified@@ -4,8 +4,12 @@ describe "Proposal embeds", type: :system do include_context "with a component" + let(:manifest_name) { "proposals" } let(:resource) { create(:proposal, component: component) } + let(:widget_path) { Decidim::EngineRouter.main_proxy(component).proposal_widget_path(resource) } - it_behaves_like "an embed resource" + it_behaves_like "an embed resource", skip_publication_checks: true + it_behaves_like "a moderated embed resource" + it_behaves_like "a withdrawn embed resource" end
decidim-sortitions/app/controllers/decidim/sortitions/widgets_controller.rb+11 −1 modified@@ -6,15 +6,25 @@ class WidgetsController < Decidim::WidgetsController helper Decidim::SanitizeHelper helper Sortitions::SortitionsHelper + def show + enforce_permission_to :embed, :sortition, sortition: model if model + + super + end + private def model - @model ||= Sortition.where(component: params[:component_id]).find(params[:sortition_id]) + @model ||= Sortition.where(component: current_component).find(params[:sortition_id]) end def iframe_url @iframe_url ||= sortition_widget_url(model) end + + def permission_class_chain + [Decidim::Sortitions::Permissions] + end end end end
decidim-sortitions/app/permissions/decidim/sortitions/permissions.rb+14 −0 modified@@ -4,12 +4,26 @@ module Decidim module Sortitions class Permissions < Decidim::DefaultPermissions def permissions + allow_embed_sortition? return permission_action unless user return Decidim::Sortitions::Admin::Permissions.new(user, permission_action, context).permissions if permission_action.scope == :admin permission_action end + + private + + def sortition + @sortition ||= context.fetch(:sortition, nil) || context.fetch(:resource, nil) + end + + # As this is a public action, we need to run this before other checks + def allow_embed_sortition? + return unless permission_action.action == :embed && permission_action.subject == :sortition && sortition + + allow! + end end end end
decidim-sortitions/spec/permissions/decidim/sortitions/permissions_spec.rb+8 −0 modified@@ -25,6 +25,14 @@ it_behaves_like "delegates permissions to", Decidim::Sortitions::Admin::Permissions end + context "when emebedding a sortition" do + let(:action) do + { scope: :public, action: :embed, subject: :sortition } + end + + it { is_expected.to be true } + end + context "when any other condition" do let(:action) do { scope: :foo, action: :blah, subject: :sortition }
decidim-sortitions/spec/system/decidim/sortitions/sortition_embeds_spec.rb+3 −1 modified@@ -4,8 +4,10 @@ describe "Sortition embeds", type: :system do include_context "with a component" + let(:manifest_name) { "sortitions" } let(:resource) { create(:sortition, component: component) } + let(:widget_path) { Decidim::EngineRouter.main_proxy(component).sortition_widget_path(resource) } - it_behaves_like "an embed resource" + it_behaves_like "an embed resource", skip_publication_checks: true end
928259cd7f57Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-qcj6-vxwx-4rqvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-27090ghsaADVISORY
- github.com/decidim/decidim/commit/1756fa639ef393ca8e8bb16221cab2e2e7875705nvdWEB
- github.com/decidim/decidim/pull/12528nvdWEB
- github.com/decidim/decidim/releases/tag/v0.27.6nvdWEB
- github.com/decidim/decidim/security/advisories/GHSA-qcj6-vxwx-4rqvnvdWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/decidim/CVE-2024-27090.ymlghsaWEB
News mentions
0No linked articles in our index yet.