discourse/spec/integration/topic_timer_message_bus_spec.rb
Mark VanLandingham 5e6d53d3bb
FIX: Gate MessageBus groups when closing topic (#40913)
## Summary

Current main still publishes topic timer reload events on /topic/<id>
without any MessageBus audience restriction. A focused request spec
against the real /message-bus/poll endpoint showed an anonymous client,
who cannot see a restricted-category topic, can subscribe from the
pre-timer message id and receive the timer-generated {reload_topic:
true} event. The payload is small, but it confirms the reported
restricted-topic existence/channel activity leak. Nearby code
demonstrates other topic MessageBus publishes are expected to merge
Topic#secure_audience_publish_messages, which the timer jobs do not do.
Patch-triage searches for the report id, GHSA terms, MessageBus/topic
timer/secure_audience/CloseTopic/OpenTopic keywords, and related
restricted-topic live-notification leaks did not find an exact
duplicate; inspected similar patches were different root
causes/components.

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/1156


Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-06-15 15:27:07 -05:00

59 lines
1.8 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe "topic timer message bus security" do
fab!(:admin)
fab!(:group)
fab!(:member, :user)
fab!(:group_user) { Fabricate(:group_user, group: group, user: member) }
fab!(:private_category) { Fabricate(:private_category, group: group) }
fab!(:topic) { Fabricate(:topic, category: private_category) }
it "does not expose restricted timer reloads", :aggregate_failures do
channel = "/topic/#{topic.id}"
close_timer =
Fabricate(
:topic_timer,
user: admin,
topic: topic,
execute_at: 1.minute.ago,
created_at: 2.minutes.ago,
)
close_last_id = MessageBus.last_id(channel)
Jobs::CloseTopic.new.execute(topic_timer_id: close_timer.id)
post "/message-bus/poll?dlp=t", params: { channel => close_last_id }
expect(response.status).to eq(200)
expect(reload_topic_messages(channel)).to be_empty
open_timer =
Fabricate(
:topic_timer,
status_type: TopicTimer.types[:open],
user: admin,
topic: topic,
execute_at: 1.minute.ago,
created_at: 2.minutes.ago,
)
open_last_id = MessageBus.last_id(channel)
Jobs::OpenTopic.new.execute(topic_timer_id: open_timer.id)
post "/message-bus/poll?dlp=t", params: { channel => open_last_id }
expect(response.status).to eq(200)
expect(reload_topic_messages(channel)).to be_empty
sign_in(member)
post "/message-bus/poll?dlp=t", params: { channel => open_last_id }
expect(response.status).to eq(200)
expect(reload_topic_messages(channel)).to be_present
end
def reload_topic_messages(channel)
response.parsed_body.select do |message|
data = message["data"]
message["channel"] == channel && data.is_a?(Hash) && data["reload_topic"] == true
end
end
end