VYPR
Medium severity4.3NVD Advisory· Published Jun 12, 2026

CVE-2026-44780

CVE-2026-44780

Description

Discourse incorrectly exposes full raw email source to category moderation group members in the review queue, bypassing the intended access controls.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Discourse incorrectly exposes full raw email source to category moderation group members in the review queue, bypassing the intended access controls.

Vulnerability

In Discourse, an open-source discussion platform, versions 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, the ReviewableQueuedPostSerializer unconditionally included payload["raw_email"] for posts that arrived via incoming email. This meant that category moderation group members viewing the review queue could read the full inbound email source (headers, sender trace, MUA, body) without needing membership in the view_raw_email_allowed_groups group, which is the trust boundary that gates the dedicated raw-email endpoint [1].

Exploitation

An attacker who is a member of a category moderation group and can access the review queue can view the raw email source of queued posts. No special authentication beyond that group membership is required. The attacker simply navigates to the review queue and the serializer supplies the raw_email field in the JSON payload [1].

Impact

Successful exploitation allows a category moderator to read the full raw email content of incoming posts, including email headers, sender trace information, mail user agent details, and the original email body. This disclosure bypasses the intended access control (view_raw_email_allowed_groups), potentially exposing sensitive information that was meant to be restricted. The CIA outcome is information disclosure [1].

Mitigation

The issue has been patched in versions 2026.1.4, 2026.3.1, 2026.4.1, and 2026.5.0-latest.1. The fix redacts raw_email from the serialized payload unless the current user is in view_raw_email_allowed_groups, and a new Guardian#can_view_raw_emails? method centralizes the access check. The frontend also hides the envelope icon click affordance for unauthorized users. Upgrading to any of these patched versions is the recommended mitigation [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
37ab8b2e3634

SECURITY: Restrict raw email in queued post payloads [backport 2026.4]

https://github.com/discourse/discourseJoffrey JAFFEUXMay 18, 2026Fixed in 2026.4.1via llm-release-walk
6 files changed · +77 4
  • app/serializers/current_user_serializer.rb+1 1 modified
    @@ -368,7 +368,7 @@ def unseen_reviewable_count
       end
     
       def can_view_raw_email
    -    scope.user.in_any_groups?(SiteSetting.view_raw_email_allowed_groups_map)
    +    scope.can_view_raw_emails?
       end
     
       def do_not_disturb_channel_position
    
  • app/serializers/reviewable_queued_post_serializer.rb+6 0 modified
    @@ -20,6 +20,12 @@ class ReviewableQueuedPostSerializer < ReviewableSerializer
         :raw_email,
       )
     
    +  def attributes
    +    data = super
    +    data[:payload]&.delete("raw_email") unless scope&.can_view_raw_emails?
    +    data
    +  end
    +
       def fancy_title
         ERB::Util.html_escape(object.payload["title"]) if object.payload&.[]("title")
       end
    
  • frontend/discourse/app/components/reviewable/queued-post.gjs+11 2 modified
    @@ -15,6 +15,11 @@ import { i18n } from "discourse-i18n";
     
     export default class ReviewableQueuedPost extends Component {
       @service modal;
    +  @service currentUser;
    +
    +  get canViewRawEmail() {
    +    return this.currentUser?.can_view_raw_email;
    +  }
     
       @action
       showRawEmail(event) {
    @@ -38,9 +43,13 @@ export default class ReviewableQueuedPost extends Component {
               {{categoryBadge @reviewable.category}}
               <ReviewableTags @tags={{@reviewable.payload.tags}} />
               {{#if @reviewable.payload.via_email}}
    -            <a href {{on "click" this.showRawEmail}} class="show-raw-email">
    +            {{#if this.canViewRawEmail}}
    +              <a href {{on "click" this.showRawEmail}} class="show-raw-email">
    +                {{icon "envelope" title="post.via_email"}}
    +              </a>
    +            {{else}}
                   {{icon "envelope" title="post.via_email"}}
    -            </a>
    +            {{/if}}
               {{/if}}
             </ReviewableTopicLink>
           </div>
    
  • lib/guardian/post_guardian.rb+5 1 modified
    @@ -398,8 +398,12 @@ def can_see_deleted_posts_for_user?
         is_staff?
       end
     
    +  def can_view_raw_emails?
    +    @user.in_any_groups?(SiteSetting.view_raw_email_allowed_groups_map)
    +  end
    +
       def can_view_raw_email?(post)
    -    post && @user.in_any_groups?(SiteSetting.view_raw_email_allowed_groups_map)
    +    post && can_view_raw_emails?
       end
     
       def can_unhide?(post)
    
  • spec/lib/guardian/post_guardian_spec.rb+20 0 modified
    @@ -1333,6 +1333,26 @@
     
           expect(Guardian.new(trust_level_0).can_view_raw_email?(post)).to be_falsey
         end
    +
    +    it "returns false when the post is nil even for an allowed user" do
    +      SiteSetting.view_raw_email_allowed_groups = "1|2|14"
    +
    +      expect(Guardian.new(trust_level_4).can_view_raw_email?(nil)).to be_falsey
    +    end
    +  end
    +
    +  describe "#can_view_raw_emails?" do
    +    it "returns true for a user in an allowed group" do
    +      SiteSetting.view_raw_email_allowed_groups = "1|2|14"
    +
    +      expect(Guardian.new(trust_level_4).can_view_raw_emails?).to be_truthy
    +    end
    +
    +    it "returns false for a user not in an allowed group" do
    +      SiteSetting.view_raw_email_allowed_groups = "1|2|14"
    +
    +      expect(Guardian.new(trust_level_0).can_view_raw_emails?).to be_falsey
    +    end
       end
     
       describe "#can_receive_post_notifications?" do
    
  • spec/serializers/reviewable_queued_post_serializer_spec.rb+34 0 modified
    @@ -74,4 +74,38 @@
           expect(raw_field[:type]).to eq(:editor)
         end
       end
    +
    +  describe "raw email visibility" do
    +    fab!(:reviewable, :reviewable_queued_post)
    +    fab!(:user)
    +    fab!(:group)
    +    fab!(:group_user) { Fabricate(:group_user, group: group, user: user) }
    +
    +    def serialized_payload(scope_user)
    +      ReviewableQueuedPostSerializer.new(
    +        reviewable,
    +        scope: Guardian.new(scope_user),
    +        root: nil,
    +      ).as_json[
    +        :payload
    +      ]
    +    end
    +
    +    it "redacts raw_email when the user is not in view_raw_email_allowed_groups" do
    +      SiteSetting.view_raw_email_allowed_groups = Group::AUTO_GROUPS[:admins].to_s
    +
    +      payload = serialized_payload(user)
    +
    +      expect(payload["via_email"]).to eq(true)
    +      expect(payload).not_to have_key("raw_email")
    +    end
    +
    +    it "exposes raw_email when the user is in view_raw_email_allowed_groups" do
    +      SiteSetting.view_raw_email_allowed_groups = "#{Group::AUTO_GROUPS[:admins]}|#{group.id}"
    +
    +      payload = serialized_payload(user)
    +
    +      expect(payload["raw_email"]).to eq("store_me")
    +    end
    +  end
     end
    
9230af44b2e6

SECURITY: Restrict raw email in queued post payloads [backport 2026.3]

https://github.com/discourse/discourseJoffrey JAFFEUXMay 18, 2026Fixed in 2026.3.1via llm-release-walk
6 files changed · +77 4
  • app/serializers/current_user_serializer.rb+1 1 modified
    @@ -359,7 +359,7 @@ def unseen_reviewable_count
       end
     
       def can_view_raw_email
    -    scope.user.in_any_groups?(SiteSetting.view_raw_email_allowed_groups_map)
    +    scope.can_view_raw_emails?
       end
     
       def do_not_disturb_channel_position
    
  • app/serializers/reviewable_queued_post_serializer.rb+6 0 modified
    @@ -20,6 +20,12 @@ class ReviewableQueuedPostSerializer < ReviewableSerializer
         :raw_email,
       )
     
    +  def attributes
    +    data = super
    +    data[:payload]&.delete("raw_email") unless scope&.can_view_raw_emails?
    +    data
    +  end
    +
       def fancy_title
         ERB::Util.html_escape(object.payload["title"]) if object.payload&.[]("title")
       end
    
  • frontend/discourse/app/components/reviewable/queued-post.gjs+11 2 modified
    @@ -15,6 +15,11 @@ import { i18n } from "discourse-i18n";
     
     export default class ReviewableQueuedPost extends Component {
       @service modal;
    +  @service currentUser;
    +
    +  get canViewRawEmail() {
    +    return this.currentUser?.can_view_raw_email;
    +  }
     
       @action
       showRawEmail(event) {
    @@ -38,9 +43,13 @@ export default class ReviewableQueuedPost extends Component {
               {{categoryBadge @reviewable.category}}
               <ReviewableTags @tags={{@reviewable.payload.tags}} />
               {{#if @reviewable.payload.via_email}}
    -            <a href {{on "click" this.showRawEmail}} class="show-raw-email">
    +            {{#if this.canViewRawEmail}}
    +              <a href {{on "click" this.showRawEmail}} class="show-raw-email">
    +                {{icon "envelope" title="post.via_email"}}
    +              </a>
    +            {{else}}
                   {{icon "envelope" title="post.via_email"}}
    -            </a>
    +            {{/if}}
               {{/if}}
             </ReviewableTopicLink>
           </div>
    
  • lib/guardian/post_guardian.rb+5 1 modified
    @@ -394,8 +394,12 @@ def can_see_deleted_posts_for_user?
         is_staff?
       end
     
    +  def can_view_raw_emails?
    +    @user.in_any_groups?(SiteSetting.view_raw_email_allowed_groups_map)
    +  end
    +
       def can_view_raw_email?(post)
    -    post && @user.in_any_groups?(SiteSetting.view_raw_email_allowed_groups_map)
    +    post && can_view_raw_emails?
       end
     
       def can_unhide?(post)
    
  • spec/lib/guardian/post_guardian_spec.rb+20 0 modified
    @@ -1293,6 +1293,26 @@
     
           expect(Guardian.new(trust_level_0).can_view_raw_email?(post)).to be_falsey
         end
    +
    +    it "returns false when the post is nil even for an allowed user" do
    +      SiteSetting.view_raw_email_allowed_groups = "1|2|14"
    +
    +      expect(Guardian.new(trust_level_4).can_view_raw_email?(nil)).to be_falsey
    +    end
    +  end
    +
    +  describe "#can_view_raw_emails?" do
    +    it "returns true for a user in an allowed group" do
    +      SiteSetting.view_raw_email_allowed_groups = "1|2|14"
    +
    +      expect(Guardian.new(trust_level_4).can_view_raw_emails?).to be_truthy
    +    end
    +
    +    it "returns false for a user not in an allowed group" do
    +      SiteSetting.view_raw_email_allowed_groups = "1|2|14"
    +
    +      expect(Guardian.new(trust_level_0).can_view_raw_emails?).to be_falsey
    +    end
       end
     
       describe "#can_receive_post_notifications?" do
    
  • spec/serializers/reviewable_queued_post_serializer_spec.rb+34 0 modified
    @@ -74,4 +74,38 @@
           expect(raw_field[:type]).to eq(:editor)
         end
       end
    +
    +  describe "raw email visibility" do
    +    fab!(:reviewable, :reviewable_queued_post)
    +    fab!(:user)
    +    fab!(:group)
    +    fab!(:group_user) { Fabricate(:group_user, group: group, user: user) }
    +
    +    def serialized_payload(scope_user)
    +      ReviewableQueuedPostSerializer.new(
    +        reviewable,
    +        scope: Guardian.new(scope_user),
    +        root: nil,
    +      ).as_json[
    +        :payload
    +      ]
    +    end
    +
    +    it "redacts raw_email when the user is not in view_raw_email_allowed_groups" do
    +      SiteSetting.view_raw_email_allowed_groups = Group::AUTO_GROUPS[:admins].to_s
    +
    +      payload = serialized_payload(user)
    +
    +      expect(payload["via_email"]).to eq(true)
    +      expect(payload).not_to have_key("raw_email")
    +    end
    +
    +    it "exposes raw_email when the user is in view_raw_email_allowed_groups" do
    +      SiteSetting.view_raw_email_allowed_groups = "#{Group::AUTO_GROUPS[:admins]}|#{group.id}"
    +
    +      payload = serialized_payload(user)
    +
    +      expect(payload["raw_email"]).to eq("store_me")
    +    end
    +  end
     end
    
36869778c0b5

SECURITY: Restrict raw email in queued post payloads [backport 2026.1]

https://github.com/discourse/discourseJoffrey JAFFEUXMay 18, 2026Fixed in 2026.1.4via llm-release-walk
6 files changed · +77 4
  • app/serializers/current_user_serializer.rb+1 1 modified
    @@ -328,7 +328,7 @@ def unseen_reviewable_count
       end
     
       def can_view_raw_email
    -    scope.user.in_any_groups?(SiteSetting.view_raw_email_allowed_groups_map)
    +    scope.can_view_raw_emails?
       end
     
       def do_not_disturb_channel_position
    
  • app/serializers/reviewable_queued_post_serializer.rb+6 0 modified
    @@ -20,6 +20,12 @@ class ReviewableQueuedPostSerializer < ReviewableSerializer
         :raw_email,
       )
     
    +  def attributes
    +    data = super
    +    data[:payload]&.delete("raw_email") unless scope&.can_view_raw_emails?
    +    data
    +  end
    +
       def fancy_title
         ERB::Util.html_escape(object.payload["title"]) if object.payload&.[]("title")
       end
    
  • frontend/discourse/app/components/reviewable-refresh/queued-post.gjs+11 2 modified
    @@ -15,6 +15,11 @@ import { i18n } from "discourse-i18n";
     
     export default class ReviewableQueuedPost extends Component {
       @service modal;
    +  @service currentUser;
    +
    +  get canViewRawEmail() {
    +    return this.currentUser?.can_view_raw_email;
    +  }
     
       @action
       showRawEmail(event) {
    @@ -38,9 +43,13 @@ export default class ReviewableQueuedPost extends Component {
               {{categoryBadge @reviewable.category}}
               <ReviewableTags @tags={{@reviewable.payload.tags}} @tagName="" />
               {{#if @reviewable.payload.via_email}}
    -            <a href {{on "click" this.showRawEmail}} class="show-raw-email">
    +            {{#if this.canViewRawEmail}}
    +              <a href {{on "click" this.showRawEmail}} class="show-raw-email">
    +                {{icon "envelope" title="post.via_email"}}
    +              </a>
    +            {{else}}
                   {{icon "envelope" title="post.via_email"}}
    -            </a>
    +            {{/if}}
               {{/if}}
             </ReviewableTopicLink>
           </div>
    
  • lib/guardian/post_guardian.rb+5 1 modified
    @@ -397,8 +397,12 @@ def can_see_deleted_posts_for_user?
         is_staff?
       end
     
    +  def can_view_raw_emails?
    +    @user.in_any_groups?(SiteSetting.view_raw_email_allowed_groups_map)
    +  end
    +
       def can_view_raw_email?(post)
    -    post && @user.in_any_groups?(SiteSetting.view_raw_email_allowed_groups_map)
    +    post && can_view_raw_emails?
       end
     
       def can_unhide?(post)
    
  • spec/lib/guardian/post_guardian_spec.rb+20 0 modified
    @@ -1268,6 +1268,26 @@
     
           expect(Guardian.new(trust_level_0).can_view_raw_email?(post)).to be_falsey
         end
    +
    +    it "returns false when the post is nil even for an allowed user" do
    +      SiteSetting.view_raw_email_allowed_groups = "1|2|14"
    +
    +      expect(Guardian.new(trust_level_4).can_view_raw_email?(nil)).to be_falsey
    +    end
    +  end
    +
    +  describe "#can_view_raw_emails?" do
    +    it "returns true for a user in an allowed group" do
    +      SiteSetting.view_raw_email_allowed_groups = "1|2|14"
    +
    +      expect(Guardian.new(trust_level_4).can_view_raw_emails?).to be_truthy
    +    end
    +
    +    it "returns false for a user not in an allowed group" do
    +      SiteSetting.view_raw_email_allowed_groups = "1|2|14"
    +
    +      expect(Guardian.new(trust_level_0).can_view_raw_emails?).to be_falsey
    +    end
       end
     
       describe "#can_receive_post_notifications?" do
    
  • spec/serializers/reviewable_queued_post_serializer_spec.rb+34 0 modified
    @@ -74,4 +74,38 @@
           expect(raw_field[:type]).to eq(:editor)
         end
       end
    +
    +  describe "raw email visibility" do
    +    fab!(:reviewable, :reviewable_queued_post)
    +    fab!(:user)
    +    fab!(:group)
    +    fab!(:group_user) { Fabricate(:group_user, group: group, user: user) }
    +
    +    def serialized_payload(scope_user)
    +      ReviewableQueuedPostSerializer.new(
    +        reviewable,
    +        scope: Guardian.new(scope_user),
    +        root: nil,
    +      ).as_json[
    +        :payload
    +      ]
    +    end
    +
    +    it "redacts raw_email when the user is not in view_raw_email_allowed_groups" do
    +      SiteSetting.view_raw_email_allowed_groups = Group::AUTO_GROUPS[:admins].to_s
    +
    +      payload = serialized_payload(user)
    +
    +      expect(payload["via_email"]).to eq(true)
    +      expect(payload).not_to have_key("raw_email")
    +    end
    +
    +    it "exposes raw_email when the user is in view_raw_email_allowed_groups" do
    +      SiteSetting.view_raw_email_allowed_groups = "#{Group::AUTO_GROUPS[:admins]}|#{group.id}"
    +
    +      payload = serialized_payload(user)
    +
    +      expect(payload["raw_email"]).to eq("store_me")
    +    end
    +  end
     end
    

Vulnerability mechanics

Root cause

"Missing access control check in ReviewableQueuedPostSerializer – the raw email payload was serialized unconditionally regardless of the viewer's group membership."

Attack vector

An attacker who is a member of a category moderation group (and can thus access the review queue) can view the full inbound email source for posts that arrived via incoming email [patch_id=5750859][patch_id=5750860][patch_id=5750861]. The `ReviewableQueuedPostSerializer` unconditionally exposed `payload["raw_email"]` in the serialized response, bypassing the `view_raw_email_allowed_groups` setting. The envelope icon in the UI was clickable even for unauthorized users, providing a deceptive UI path to the same data.

Affected code

`ReviewableQueuedPostSerializer` (app/serializers/reviewable_queued_post_serializer.rb) unconditionally included `payload["raw_email"]` for queued posts from incoming email. Category moderation group members reviewing queued posts could thus read full inbound email source via the review queue without being in `view_raw_email_allowed_groups`. The frontend component `reviewable/queued-post.gjs` also rendered a clickable envelope link that exposed the raw email regardless of group membership.

What the fix does

The serializer (`app/serializers/reviewable_queued_post_serializer.rb`) now deletes `raw_email` from the payload in its `attributes` method unless `scope.can_view_raw_emails?` returns true, mirroring the existing `Guardian#can_view_raw_email` check [patch_id=5750859]. A new `Guardian#can_view_raw_emails?` method centralizes this group-check logic, and the frontend component conditionally removes the click action from the envelope icon for users without raw email access [patch_id=5750860][patch_id=5750861].

Preconditions

  • authThe attacker must be a member of a category moderation group that can access the review queue.
  • inputA post must exist in the review queue that was created via incoming email (i.e., has `via_email` set and a `raw_email` payload).
  • configThe attacker must not be in the `view_raw_email_allowed_groups` site setting.

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.