SECURITY: BCC active user emails from group SMTP (#19725)
When sending emails out via group SMTP, if we are sending them to non-staged users we want to mask those emails with BCC, just so we don't expose them to anyone we shouldn't. Staged users are ones that have likely only interacted with support via email, and will likely include other people who were CC'd on the original email to the group. Co-authored-by: Martin Brennan <martin@discourse.org>
This commit is contained in:
parent
f4ab3f4543
commit
ab3a032b4b
|
@ -44,6 +44,12 @@ module Jobs
|
||||||
EmailAddressValidator.valid_value?(address)
|
EmailAddressValidator.valid_value?(address)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Mask the email addresses of non-staged users so
|
||||||
|
# they are not revealed unnecessarily when we are sending
|
||||||
|
# the email notification out.
|
||||||
|
bcc_addresses = User.not_staged.with_email(cc_addresses).pluck(:email)
|
||||||
|
cc_addresses = cc_addresses - bcc_addresses
|
||||||
|
|
||||||
# There is a rare race condition causing the Imap::Sync class to create
|
# There is a rare race condition causing the Imap::Sync class to create
|
||||||
# an incoming email and associated post/topic, which then kicks off
|
# an incoming email and associated post/topic, which then kicks off
|
||||||
# the PostAlerter to notify others in the PM about a reply in the topic,
|
# the PostAlerter to notify others in the PM about a reply in the topic,
|
||||||
|
@ -66,7 +72,13 @@ module Jobs
|
||||||
# The EmailLog record created by the sender will have the raw email
|
# The EmailLog record created by the sender will have the raw email
|
||||||
# stored, the group smtp ID, and any cc addresses recorded for later
|
# stored, the group smtp ID, and any cc addresses recorded for later
|
||||||
# cross referencing.
|
# cross referencing.
|
||||||
message = GroupSmtpMailer.send_mail(group, email, post, cc_addresses)
|
message = GroupSmtpMailer.send_mail(
|
||||||
|
group,
|
||||||
|
email,
|
||||||
|
post,
|
||||||
|
cc_addresses: cc_addresses,
|
||||||
|
bcc_addresses: bcc_addresses
|
||||||
|
)
|
||||||
Email::Sender.new(message, :group_smtp, recipient_user).send
|
Email::Sender.new(message, :group_smtp, recipient_user).send
|
||||||
|
|
||||||
# Create an incoming email record to avoid importing again from IMAP
|
# Create an incoming email record to avoid importing again from IMAP
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class GroupSmtpMailer < ActionMailer::Base
|
class GroupSmtpMailer < ActionMailer::Base
|
||||||
include Email::BuildEmailHelper
|
include Email::BuildEmailHelper
|
||||||
|
|
||||||
def send_mail(from_group, to_address, post, cc_addresses = nil)
|
def send_mail(from_group, to_address, post, cc_addresses: nil, bcc_addresses: nil)
|
||||||
raise 'SMTP is disabled' if !SiteSetting.enable_smtp
|
raise 'SMTP is disabled' if !SiteSetting.enable_smtp
|
||||||
|
|
||||||
op_incoming_email = post.topic.first_post.incoming_email
|
op_incoming_email = post.topic.first_post.incoming_email
|
||||||
|
@ -46,7 +46,8 @@ class GroupSmtpMailer < ActionMailer::Base
|
||||||
from: from_group.smtp_from_address,
|
from: from_group.smtp_from_address,
|
||||||
from_alias: I18n.t('email_from_without_site', group_name: group_name),
|
from_alias: I18n.t('email_from_without_site', group_name: group_name),
|
||||||
html_override: html_override(post),
|
html_override: html_override(post),
|
||||||
cc: cc_addresses
|
cc: cc_addresses,
|
||||||
|
bcc: bcc_addresses
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -144,6 +144,7 @@ end
|
||||||
# topic_id :integer
|
# topic_id :integer
|
||||||
# bounce_error_code :string
|
# bounce_error_code :string
|
||||||
# smtp_transaction_response :string(500)
|
# smtp_transaction_response :string(500)
|
||||||
|
# bcc_addresses :text
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddBccAddressesToEmailLog < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
add_column :email_logs, :bcc_addresses, :text, null: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -141,7 +141,8 @@ module Email
|
||||||
body: body,
|
body: body,
|
||||||
charset: 'UTF-8',
|
charset: 'UTF-8',
|
||||||
from: from_value,
|
from: from_value,
|
||||||
cc: @opts[:cc]
|
cc: @opts[:cc],
|
||||||
|
bcc: @opts[:bcc]
|
||||||
}
|
}
|
||||||
|
|
||||||
args[:delivery_method_options] = @opts[:delivery_method_options] if @opts[:delivery_method_options]
|
args[:delivery_method_options] = @opts[:delivery_method_options] if @opts[:delivery_method_options]
|
||||||
|
|
|
@ -98,6 +98,10 @@ module Email
|
||||||
email_log.cc_user_ids = User.with_email(cc_addresses).pluck(:id)
|
email_log.cc_user_ids = User.with_email(cc_addresses).pluck(:id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if bcc_addresses.any?
|
||||||
|
email_log.bcc_addresses = bcc_addresses.join(";")
|
||||||
|
end
|
||||||
|
|
||||||
host = Email::Sender.host_for(Discourse.base_url)
|
host = Email::Sender.host_for(Discourse.base_url)
|
||||||
|
|
||||||
post_id = header_value('X-Discourse-Post-Id')
|
post_id = header_value('X-Discourse-Post-Id')
|
||||||
|
@ -300,6 +304,12 @@ module Email
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def bcc_addresses
|
||||||
|
@bcc_addresses ||= begin
|
||||||
|
@message.try(:bcc) || []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.host_for(base_url)
|
def self.host_for(base_url)
|
||||||
host = "localhost"
|
host = "localhost"
|
||||||
if base_url.present?
|
if base_url.present?
|
||||||
|
|
|
@ -36,7 +36,16 @@ RSpec.describe Jobs::GroupSmtpEmail do
|
||||||
|
|
||||||
it "sends an email using the GroupSmtpMailer and Email::Sender" do
|
it "sends an email using the GroupSmtpMailer and Email::Sender" do
|
||||||
message = Mail::Message.new(body: "hello", to: "myemail@example.invalid")
|
message = Mail::Message.new(body: "hello", to: "myemail@example.invalid")
|
||||||
GroupSmtpMailer.expects(:send_mail).with(group, "test@test.com", post, ["otherguy@test.com", "cormac@lit.com"]).returns(message)
|
GroupSmtpMailer
|
||||||
|
.expects(:send_mail)
|
||||||
|
.with(
|
||||||
|
group,
|
||||||
|
"test@test.com",
|
||||||
|
post,
|
||||||
|
cc_addresses: %w[otherguy@test.com cormac@lit.com],
|
||||||
|
bcc_addresses: [],
|
||||||
|
)
|
||||||
|
.returns(message)
|
||||||
subject.execute(args)
|
subject.execute(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -172,11 +181,27 @@ RSpec.describe Jobs::GroupSmtpEmail do
|
||||||
it "has the cc_addresses and cc_user_ids filled in correctly" do
|
it "has the cc_addresses and cc_user_ids filled in correctly" do
|
||||||
subject.execute(args)
|
subject.execute(args)
|
||||||
expect(ActionMailer::Base.deliveries.count).to eq(1)
|
expect(ActionMailer::Base.deliveries.count).to eq(1)
|
||||||
expect(ActionMailer::Base.deliveries.last.subject).to eq("Re: Help I need support")
|
sent_mail = ActionMailer::Base.deliveries.last
|
||||||
|
expect(sent_mail.subject).to eq("Re: Help I need support")
|
||||||
|
expect(sent_mail.cc).to eq(["otherguy@test.com", "cormac@lit.com"])
|
||||||
email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id)
|
email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id)
|
||||||
expect(email_log.cc_addresses).to eq("otherguy@test.com;cormac@lit.com")
|
expect(email_log.cc_addresses).to eq("otherguy@test.com;cormac@lit.com")
|
||||||
expect(email_log.cc_user_ids).to match_array([staged1.id, staged2.id])
|
expect(email_log.cc_user_ids).to match_array([staged1.id, staged2.id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "where cc_addresses match non-staged users, convert to bcc_addresses" do
|
||||||
|
staged2.update!(staged: false, active: true)
|
||||||
|
subject.execute(args)
|
||||||
|
expect(ActionMailer::Base.deliveries.count).to eq(1)
|
||||||
|
sent_mail = ActionMailer::Base.deliveries.last
|
||||||
|
expect(sent_mail.subject).to eq("Re: Help I need support")
|
||||||
|
expect(sent_mail.cc).to eq(["otherguy@test.com"])
|
||||||
|
expect(sent_mail.bcc).to eq(["cormac@lit.com"])
|
||||||
|
email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id)
|
||||||
|
expect(email_log.cc_addresses).to eq("otherguy@test.com")
|
||||||
|
expect(email_log.bcc_addresses).to eq("cormac@lit.com")
|
||||||
|
expect(email_log.cc_user_ids).to match_array([staged1.id])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the post in the argument is the OP" do
|
context "when the post in the argument is the OP" do
|
||||||
|
|
|
@ -35,6 +35,7 @@ RSpec.describe GroupSmtpMailer do
|
||||||
Message-ID: <a52f67a3d3560f2a35276cda8519b10b595623bcb66912bb92df6651ad5f75be@mail.gmail.com>
|
Message-ID: <a52f67a3d3560f2a35276cda8519b10b595623bcb66912bb92df6651ad5f75be@mail.gmail.com>
|
||||||
Subject: Hello from John
|
Subject: Hello from John
|
||||||
To: "bugs@gmail.com" <bugs@gmail.com>
|
To: "bugs@gmail.com" <bugs@gmail.com>
|
||||||
|
Cc: someotherperson@test.com
|
||||||
Content-Type: text/plain; charset="UTF-8"
|
Content-Type: text/plain; charset="UTF-8"
|
||||||
|
|
||||||
Hello,
|
Hello,
|
||||||
|
@ -75,6 +76,7 @@ RSpec.describe GroupSmtpMailer do
|
||||||
|
|
||||||
sent_mail = ActionMailer::Base.deliveries[0]
|
sent_mail = ActionMailer::Base.deliveries[0]
|
||||||
expect(sent_mail.to).to contain_exactly('john@doe.com')
|
expect(sent_mail.to).to contain_exactly('john@doe.com')
|
||||||
|
expect(sent_mail.cc).to contain_exactly('someotherperson@test.com')
|
||||||
expect(sent_mail.reply_to).to eq(nil)
|
expect(sent_mail.reply_to).to eq(nil)
|
||||||
expect(sent_mail.subject).to eq('Re: Hello from John')
|
expect(sent_mail.subject).to eq('Re: Hello from John')
|
||||||
expect(sent_mail.to_s).to include(raw)
|
expect(sent_mail.to_s).to include(raw)
|
||||||
|
|
Loading…
Reference in New Issue