CVE-2026-44783
Description
Discourse whisper reply handling allows non-whisperers to post into staff-only whisper channels, 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.
Discourse whisper reply handling allows non-whisperers to post into staff-only whisper channels, fixed in versions 2026.1.4, 2026.3.1, 2026.4.1, and 2026.5.0-latest.1.
Vulnerability
Discourse versions from 2026.1.0-latest to before 2026.1.4, 2026.3.0-latest to before 2026.3.1, and 2026.4.0-latest to before 2026.4.1 contain a flaw in how replies to whisper posts are handled. This allows authenticated users outside the groups configured in whispers_allowed_groups to post into a topic's staff-only whisper channel. Only sites that have whispers enabled are affected. [1]
Exploitation
An attacker needs to be an authenticated user who is not a member of the groups specified in the whispers_allowed_groups setting. By replying to an existing whisper post, the attacker can inject content into the staff-only whisper channel. [1]
Impact
The injected content becomes visible to whisperers (typically staff) alongside legitimate whispers. This enables unauthorized users to post into the staff-only area, potentially leading to misinformation or confusion. [1]
Mitigation
The issue is patched in Discourse versions 2026.1.4, 2026.3.1, 2026.4.1, and 2026.5.0-latest.1. As a workaround, administrators of sites that do not need whispers can clear the whispers_allowed_groups setting until the patch is applied; sites with no groups configured are not affected. [1]
AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1- Range: >=2026.1.0, <2026.1.4 || >=2026.3.0, <2026.3.1 || >=2026.4.0, <2026.4.1
Patches
3472d6dd0f083SECURITY: Replying to a whisper lets non-whisperers create staff-only whisper posts [backport 2026.4]
5 files changed · +59 −10
lib/post_creator.rb+9 −1 modified@@ -333,7 +333,15 @@ def self.set_reply_info(post) if reply_info.present? post.reply_to_user_id ||= reply_info.user_id whisper_type = Post.types[:whisper] - post.post_type = whisper_type if reply_info.post_type == whisper_type + + if reply_info.post_type == whisper_type + if post.acting_user&.whisperer? + post.post_type = whisper_type + else + post.errors.add(:base, I18n.t(:topic_not_found)) + throw :abort + end + end end end
spec/lib/post_creator_spec.rb+9 −7 modified@@ -692,20 +692,22 @@ describe "whisper" do fab!(:topic) { Fabricate(:topic, user: user) } + before { SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}" } + it "whispers do not mess up the public view" do freeze_time_safe - first = PostCreator.new(user, topic_id: topic.id, raw: "this is the first post").create + first = PostCreator.new(admin, topic_id: topic.id, raw: "this is the first post").create freeze_time 1.year.from_now - user_stat = user.user_stat + user_stat = admin.user_stat whisper = PostCreator.new( - user, + admin, topic_id: topic.id, - reply_to_post_number: 1, + reply_to_post_number: first.post_number, post_type: Post.types[:whisper], raw: "this is a whispered reply", ).create @@ -718,7 +720,7 @@ whisper_reply = PostCreator.new( - user, + admin, topic_id: topic.id, reply_to_post_number: whisper.post_number, post_type: Post.types[:regular], @@ -730,8 +732,8 @@ expect(user_stat.reload.post_count).to eq(0) - user.reload - expect(user.last_posted_at).to eq_time(1.year.ago) + admin.reload + expect(admin.last_posted_at).to eq_time(1.year.ago) # date is not precise enough in db whisper_reply.reload
spec/models/user_summary_spec.rb+5 −2 modified@@ -135,8 +135,11 @@ # Don't include replies to whispers topic3 = create_post(user: topic1.user).topic topic3_post = create_post(topic: topic3, post_type: Post.types[:whisper]) - topic3_reply = - create_post(topic: topic3, reply_to_post_number: topic3_post.post_number, user: topic3.user) + topic3_reply = create_post(topic: topic3, user: topic1.user) + topic3_reply.update_columns( + reply_to_post_number: topic3_post.post_number, + post_type: Post.types[:whisper], + ) # Don't include replies to private messages replied_to_user = Fabricate(:user)
spec/requests/posts_controller_spec.rb+35 −0 modified@@ -1819,6 +1819,41 @@ expect(topic.visible).to eq(true) end + it "prevents regular users from replying to whispers" do + sign_in(admin) + post "/posts.json", + params: { + raw: "this is the first post with enough words", + title: "this is a topic title for whispers", + } + expect(response.status).to eq(200) + + topic_id = response.parsed_body["topic_id"] + post "/posts.json", + params: { + raw: "this is a staff-only whisper", + topic_id: topic_id, + reply_to_post_number: 1, + whisper: true, + } + expect(response.status).to eq(200) + + whisper_post_number = response.parsed_body["post_number"] + sign_in(user) + + expect do + post "/posts.json", + params: { + raw: "replying to a whisper over http", + topic_id: topic_id, + reply_to_post_number: whisper_post_number, + } + end.not_to change { Post.count } + + expect(response.status).to eq(422) + expect(response.parsed_body["errors"]).to include(I18n.t(:topic_not_found)) + end + describe "posts_controller_create_user modifier" do fab!(:different_user, :admin)
spec/services/post_alerter_spec.rb+1 −0 modified@@ -1699,6 +1699,7 @@ def set_topic_notification_level(user, topic, level_name) end it "notifies staff user about whispered reply" do + SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}" admin1 = Fabricate(:admin) admin2 = Fabricate(:admin)
ae1b1850582fSECURITY: Replying to a whisper lets non-whisperers create staff-only whisper posts [backport 2026.3]
5 files changed · +59 −10
lib/post_creator.rb+9 −1 modified@@ -333,7 +333,15 @@ def self.set_reply_info(post) if reply_info.present? post.reply_to_user_id ||= reply_info.user_id whisper_type = Post.types[:whisper] - post.post_type = whisper_type if reply_info.post_type == whisper_type + + if reply_info.post_type == whisper_type + if post.acting_user&.whisperer? + post.post_type = whisper_type + else + post.errors.add(:base, I18n.t(:topic_not_found)) + throw :abort + end + end end end
spec/lib/post_creator_spec.rb+9 −7 modified@@ -692,20 +692,22 @@ describe "whisper" do fab!(:topic) { Fabricate(:topic, user: user) } + before { SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}" } + it "whispers do not mess up the public view" do freeze_time_safe - first = PostCreator.new(user, topic_id: topic.id, raw: "this is the first post").create + first = PostCreator.new(admin, topic_id: topic.id, raw: "this is the first post").create freeze_time 1.year.from_now - user_stat = user.user_stat + user_stat = admin.user_stat whisper = PostCreator.new( - user, + admin, topic_id: topic.id, - reply_to_post_number: 1, + reply_to_post_number: first.post_number, post_type: Post.types[:whisper], raw: "this is a whispered reply", ).create @@ -718,7 +720,7 @@ whisper_reply = PostCreator.new( - user, + admin, topic_id: topic.id, reply_to_post_number: whisper.post_number, post_type: Post.types[:regular], @@ -730,8 +732,8 @@ expect(user_stat.reload.post_count).to eq(0) - user.reload - expect(user.last_posted_at).to eq_time(1.year.ago) + admin.reload + expect(admin.last_posted_at).to eq_time(1.year.ago) # date is not precise enough in db whisper_reply.reload
spec/models/user_summary_spec.rb+5 −2 modified@@ -135,8 +135,11 @@ # Don't include replies to whispers topic3 = create_post(user: topic1.user).topic topic3_post = create_post(topic: topic3, post_type: Post.types[:whisper]) - topic3_reply = - create_post(topic: topic3, reply_to_post_number: topic3_post.post_number, user: topic3.user) + topic3_reply = create_post(topic: topic3, user: topic1.user) + topic3_reply.update_columns( + reply_to_post_number: topic3_post.post_number, + post_type: Post.types[:whisper], + ) # Don't include replies to private messages replied_to_user = Fabricate(:user)
spec/requests/posts_controller_spec.rb+35 −0 modified@@ -1734,6 +1734,41 @@ expect(topic.visible).to eq(true) end + it "prevents regular users from replying to whispers" do + sign_in(admin) + post "/posts.json", + params: { + raw: "this is the first post with enough words", + title: "this is a topic title for whispers", + } + expect(response.status).to eq(200) + + topic_id = response.parsed_body["topic_id"] + post "/posts.json", + params: { + raw: "this is a staff-only whisper", + topic_id: topic_id, + reply_to_post_number: 1, + whisper: true, + } + expect(response.status).to eq(200) + + whisper_post_number = response.parsed_body["post_number"] + sign_in(user) + + expect do + post "/posts.json", + params: { + raw: "replying to a whisper over http", + topic_id: topic_id, + reply_to_post_number: whisper_post_number, + } + end.not_to change { Post.count } + + expect(response.status).to eq(422) + expect(response.parsed_body["errors"]).to include(I18n.t(:topic_not_found)) + end + describe "posts_controller_create_user modifier" do fab!(:different_user, :admin)
spec/services/post_alerter_spec.rb+1 −0 modified@@ -1694,6 +1694,7 @@ def set_topic_notification_level(user, topic, level_name) end it "notifies staff user about whispered reply" do + SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}" admin1 = Fabricate(:admin) admin2 = Fabricate(:admin)
a832e443de07SECURITY: Replying to a whisper lets non-whisperers create staff-only whisper posts [backport 2026.1]
5 files changed · +59 −10
lib/post_creator.rb+9 −1 modified@@ -320,7 +320,15 @@ def self.set_reply_info(post) if reply_info.present? post.reply_to_user_id ||= reply_info.user_id whisper_type = Post.types[:whisper] - post.post_type = whisper_type if reply_info.post_type == whisper_type + + if reply_info.post_type == whisper_type + if post.acting_user&.whisperer? + post.post_type = whisper_type + else + post.errors.add(:base, I18n.t(:topic_not_found)) + throw :abort + end + end end end
spec/lib/post_creator_spec.rb+9 −7 modified@@ -692,20 +692,22 @@ describe "whisper" do fab!(:topic) { Fabricate(:topic, user: user) } + before { SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}" } + it "whispers do not mess up the public view" do freeze_time_safe - first = PostCreator.new(user, topic_id: topic.id, raw: "this is the first post").create + first = PostCreator.new(admin, topic_id: topic.id, raw: "this is the first post").create freeze_time 1.year.from_now - user_stat = user.user_stat + user_stat = admin.user_stat whisper = PostCreator.new( - user, + admin, topic_id: topic.id, - reply_to_post_number: 1, + reply_to_post_number: first.post_number, post_type: Post.types[:whisper], raw: "this is a whispered reply", ).create @@ -718,7 +720,7 @@ whisper_reply = PostCreator.new( - user, + admin, topic_id: topic.id, reply_to_post_number: whisper.post_number, post_type: Post.types[:regular], @@ -730,8 +732,8 @@ expect(user_stat.reload.post_count).to eq(0) - user.reload - expect(user.last_posted_at).to eq_time(1.year.ago) + admin.reload + expect(admin.last_posted_at).to eq_time(1.year.ago) # date is not precise enough in db whisper_reply.reload
spec/models/user_summary_spec.rb+5 −2 modified@@ -135,8 +135,11 @@ # Don't include replies to whispers topic3 = create_post(user: topic1.user).topic topic3_post = create_post(topic: topic3, post_type: Post.types[:whisper]) - topic3_reply = - create_post(topic: topic3, reply_to_post_number: topic3_post.post_number, user: topic3.user) + topic3_reply = create_post(topic: topic3, user: topic1.user) + topic3_reply.update_columns( + reply_to_post_number: topic3_post.post_number, + post_type: Post.types[:whisper], + ) # Don't include replies to private messages replied_to_user = Fabricate(:user)
spec/requests/posts_controller_spec.rb+35 −0 modified@@ -1657,6 +1657,41 @@ expect(topic.visible).to eq(true) end + it "prevents regular users from replying to whispers" do + sign_in(admin) + post "/posts.json", + params: { + raw: "this is the first post with enough words", + title: "this is a topic title for whispers", + } + expect(response.status).to eq(200) + + topic_id = response.parsed_body["topic_id"] + post "/posts.json", + params: { + raw: "this is a staff-only whisper", + topic_id: topic_id, + reply_to_post_number: 1, + whisper: true, + } + expect(response.status).to eq(200) + + whisper_post_number = response.parsed_body["post_number"] + sign_in(user) + + expect do + post "/posts.json", + params: { + raw: "replying to a whisper over http", + topic_id: topic_id, + reply_to_post_number: whisper_post_number, + } + end.not_to change { Post.count } + + expect(response.status).to eq(422) + expect(response.parsed_body["errors"]).to include(I18n.t(:topic_not_found)) + end + describe "posts_controller_create_user modifier" do fab!(:different_user, :admin)
spec/services/post_alerter_spec.rb+1 −0 modified@@ -1685,6 +1685,7 @@ def set_topic_notification_level(user, topic, level_name) end it "notifies staff user about whispered reply" do + SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}" admin1 = Fabricate(:admin) admin2 = Fabricate(:admin)
Vulnerability mechanics
Root cause
"Missing authorization check when replying to a whisper post allows non-whisperer users to create staff-only whisper posts."
Attack vector
An authenticated user who does **not** belong to the groups configured in `whispers_allowed_groups` can reply to an existing whisper post in a topic. When the `PostCreator` processes the reply, the original code unconditionally set `post.post_type = whisper_type` whenever the parent post was a whisper, turning the reply into a staff-only whisper post. This allows the non-whisperer to inject content that becomes visible to whisperers (typically staff) alongside legitimate whispers. The attacker only needs network access to the Discourse instance, a valid login, and the `reply_to_post_number` pointing to a whisper post [patch_id=5750855].
What the fix does
The patch modifies `PostCreator.set_reply_info` in `lib/post_creator.rb` to check `post.acting_user&.whisperer?` before propagating the whisper post type. If the acting user is not a whisperer, the method adds a `topic_not_found` error and aborts the creation with `throw :abort`, instead of blindly inheriting the whisper type [patch_id=5750855]. The same logic is backported across all affected release branches. Tests were also updated to require `whispers_allowed_groups` to be explicitly set to a staff group and to verify that a regular user cannot create a post replying to a whisper [patch_id=5750855].
Preconditions
- configThe Discourse site must have whispers enabled (whispers_allowed_groups configured).
- authThe attacker must be an authenticated user who is NOT a member of the groups listed in whispers_allowed_groups.
- inputA whisper post must already exist in a topic that the attacker can access.
Generated on Jun 12, 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.