VYPR
Medium severity5.4NVD Advisory· Published Jun 12, 2026

CVE-2026-44783

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

Patches

3
472d6dd0f083

SECURITY: Replying to a whisper lets non-whisperers create staff-only whisper posts [backport 2026.4]

https://github.com/discourse/discoursediscourse-patch-triage[bot]May 18, 2026Fixed in 2026.4.1via llm-release-walk
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)
     
    
ae1b1850582f

SECURITY: Replying to a whisper lets non-whisperers create staff-only whisper posts [backport 2026.3]

https://github.com/discourse/discoursediscourse-patch-triage[bot]May 18, 2026Fixed in 2026.3.1via llm-release-walk
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)
     
    
a832e443de07

SECURITY: Replying to a whisper lets non-whisperers create staff-only whisper posts [backport 2026.1]

https://github.com/discourse/discoursediscourse-patch-triage[bot]May 18, 2026Fixed in 2026.1.4via llm-release-walk
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

1

News mentions

0

No linked articles in our index yet.