0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-08 17:53:55 +08:00
discourse/spec/requests/admin/email_controller_spec.rb
Mark VanLandingham 7d6d383b4d
FIX: Render hashtag slugs as text nodes in Email::Styles (#42352)
## Summary

Email::Styles now appends hashtag slugs as Nokogiri text nodes instead
of interpolating them into an HTML string that is re-parsed. This
prevents an entity-decoded data-slug attribute from being turned back
into executable markup when the admin digest preview renders the
resulting email HTML in an unsandboxed iframe. The fix also updates the
sender expectation to reflect the intentional removal of the cosmetic
inner span while preserving the plain hashtag text.

## Source

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

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-08-05 09:08:24 -05:00

423 lines
13 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe Admin::EmailController do
fab!(:admin)
fab!(:moderator)
fab!(:user)
describe "#server_settings" do
context "when logged in as an admin" do
before do
sign_in(admin)
Admin::EmailController
.any_instance
.expects(:action_mailer_settings)
.returns(username: "username", password: "secret")
end
it "does not include the password in the response" do
get "/admin/email/server-settings.json"
mail_settings = response.parsed_body["settings"]
expect(mail_settings.select { |setting| setting["name"] == "password" }).to be_empty
end
end
shared_examples "email settings inaccessible" do
it "denies access with a 404 response" do
get "/admin/email/server-settings.json"
expect(response.status).to eq(404)
expect(response.parsed_body["settings"]).to be_nil
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
end
end
context "when logged in as a moderator" do
before { sign_in(moderator) }
include_examples "email settings inaccessible"
end
context "when logged in as a non-staff user" do
before { sign_in(user) }
include_examples "email settings inaccessible"
end
end
describe "#smtp_should_reject" do
before { sign_in(admin) }
it "does not reject with no params" do
get "/admin/email/smtp_should_reject.json"
expect(response.status).to eq(200)
result = response.parsed_body
expect(result["reject"]).to be false
end
it "does not reject with blank to/from" do
get "/admin/email/smtp_should_reject.json", params: { from: "", to: "" }
expect(response.status).to eq(200)
result = response.parsed_body
expect(result["reject"]).to be false
end
it "does not reject with blank to" do
get "/admin/email/smtp_should_reject.json", params: { from: "bilbo@shire.net", to: "" }
expect(response.status).to eq(200)
result = response.parsed_body
expect(result["reject"]).to be false
end
it "does not reject with blank from" do
get "/admin/email/smtp_should_reject.json", params: { from: "", to: "frodo@shire.net" }
expect(response.status).to eq(200)
result = response.parsed_body
expect(result["reject"]).to be false
end
it "does not reject with to/from specified" do
get "/admin/email/smtp_should_reject.json",
params: {
from: "bilbo@shire.net",
to: "frodo@shire.net",
}
expect(response.status).to eq(200)
result = response.parsed_body
expect(result["reject"]).to be false
end
end
describe "#test" do
context "when logged in as an admin" do
before { sign_in(admin) }
it "raises an error without the email parameter" do
post "/admin/email/test.json"
expect(response.status).to eq(400)
end
context "with an email address" do
it "enqueues a test email job" do
post "/admin/email/test.json", params: { email_address: "eviltrout@test.domain" }
expect(response.status).to eq(200)
expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(
"eviltrout@test.domain",
)
end
end
context "with SiteSetting.disable_emails" do
fab!(:eviltrout, :evil_trout)
fab!(:admin)
it 'bypasses disable when setting is "yes"' do
SiteSetting.disable_emails = "yes"
post "/admin/email/test.json", params: { email_address: admin.email }
expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(admin.email)
incoming = response.parsed_body
expect(incoming["sent_test_email_message"]).to eq(I18n.t("admin.email.sent_test"))
end
it 'bypasses disable when setting is "non-staff"' do
SiteSetting.disable_emails = "non-staff"
post "/admin/email/test.json", params: { email_address: eviltrout.email }
expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(eviltrout.email)
incoming = response.parsed_body
expect(incoming["sent_test_email_message"]).to eq(I18n.t("admin.email.sent_test"))
end
it 'works when setting is "no"' do
SiteSetting.disable_emails = "no"
post "/admin/email/test.json", params: { email_address: eviltrout.email }
expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(eviltrout.email)
incoming = response.parsed_body
expect(incoming["sent_test_email_message"]).to eq(I18n.t("admin.email.sent_test"))
end
end
end
shared_examples "email tests not allowed" do
it "prevents email tests with a 404 response" do
post "/admin/email/test.json", params: { email_address: "eviltrout@test.domain" }
expect(response.status).to eq(404)
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
end
end
context "when logged in as a moderator" do
before { sign_in(moderator) }
include_examples "email tests not allowed"
end
context "when logged in as a non-staff user" do
before { sign_in(user) }
include_examples "email tests not allowed"
end
end
describe "#preview_digest" do
context "when logged in as an admin" do
before { sign_in(admin) }
it "raises an error without the last_seen_at parameter" do
get "/admin/email/preview-digest.json"
expect(response.status).to eq(400)
end
it "returns the right response when username is invalid" do
get "/admin/email/preview-digest.json",
params: {
last_seen_at: 1.week.ago,
username: "somerandomeusername",
}
expect(response.status).to eq(400)
end
it "previews the digest" do
get "/admin/email/preview-digest.json",
params: {
last_seen_at: 1.week.ago,
username: admin.username,
}
expect(response.status).to eq(200)
end
it "does not turn a post attribute into executable HTML" do
attacker = Fabricate(:user, trust_level: TrustLevel[1])
payload =
'<a class="hashtag-cooked" data-slug="&lt;img src=x onerror=alert(1)&gt;">ignored</a>'
freeze_time 1.day.ago do
sign_in(attacker)
post "/posts.json", params: { title: "Attacker-created topic", raw: payload }
expect(response.status).to eq(200)
end
sign_in(admin)
get "/admin/email/preview-digest.json",
params: {
last_seen_at: 1.week.ago,
username: admin.username,
}
expect(response.status).to eq(200)
preview = Nokogiri::HTML5.parse(response.parsed_body["html_content"])
expect(preview.at_css("img[onerror='alert(1)']")).to be_nil
end
end
shared_examples "preview digest inaccessible" do
it "denies access with a 404 response" do
get "/admin/email/preview-digest.json",
params: {
last_seen_at: 1.week.ago,
username: moderator.username,
}
expect(response.status).to eq(404)
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
end
end
context "when logged in as a moderator" do
before { sign_in(moderator) }
include_examples "preview digest inaccessible"
end
context "when logged in as a non-staff user" do
before { sign_in(user) }
include_examples "preview digest inaccessible"
end
end
describe "#send_digest" do
context "when logged in as an admin" do
before { sign_in(admin) }
it "sends the digest" do
post "/admin/email/send-digest.json",
params: {
last_seen_at: 1.week.ago,
username: admin.username,
email: admin.email,
}
expect(response.status).to eq(200)
end
it "sends the digest to the requested email without unsubscribe links" do
Fabricate(:topic, created_at: 1.day.ago)
ActionMailer::Base.deliveries.clear
expect {
post "/admin/email/send-digest.json",
params: {
last_seen_at: 1.month.ago,
username: user.username,
email: "attacker@evil.com",
}
}.not_to change { UnsubscribeKey.count }
expect(response.status).to eq(200)
expect(response.parsed_body["success"]).to eq("OK")
message = ActionMailer::Base.deliveries.last
expect(message.to).to eq(["attacker@evil.com"])
expect(message.html_part.body.to_s).not_to include("/email/unsubscribe/")
expect(message.text_part.body.to_s).not_to include("/email/unsubscribe/")
expect(message.header["List-Unsubscribe"].to_s).to be_blank
expect(message.header["List-Unsubscribe-Post"].to_s).to be_blank
end
end
end
describe "#handle_mail" do
context "when logged in as an admin" do
before { sign_in(admin) }
it "returns a bad request if neither email parameter is present" do
post "/admin/email/handle_mail.json"
expect(response.status).to eq(400)
expect(response.body).to include("param is missing")
end
it "should enqueue the right job, and show a deprecation warning (email_encoded param should be used)" do
expect_enqueued_with(
job: :process_email,
args: {
mail: email("cc"),
retry_on_rate_limit: true,
source: :handle_mail,
},
) { post "/admin/email/handle_mail.json", params: { email: email("cc") } }
expect(response.status).to eq(200)
expect(response.body).to eq(
"warning: the email parameter is deprecated. all POST requests to this route should be sent with a base64 strict encoded email_encoded parameter instead. email has been received and is queued for processing",
)
end
it "should enqueue the right job, decoding the raw email param" do
expect_enqueued_with(
job: :process_email,
args: {
mail: email("cc"),
retry_on_rate_limit: true,
source: :handle_mail,
},
) do
post "/admin/email/handle_mail.json",
params: {
email_encoded: Base64.strict_encode64(email("cc")),
}
end
expect(response.status).to eq(200)
expect(response.body).to eq("email has been received and is queued for processing")
end
it "normalizes invalid UTF-8 bytes before enqueueing" do
post "/admin/email/handle_mail.json",
params: {
email_encoded: Base64.strict_encode64(email("encoding_undefined_conversion")),
}
expect(response.status).to eq(200)
expect(response.body).to eq("email has been received and is queued for processing")
end
end
shared_examples "email handling not allowed" do
it "prevents email handling with a 404 response" do
post "/admin/email/handle_mail.json",
params: {
email_encoded: Base64.strict_encode64(email("cc")),
}
expect(response.status).to eq(404)
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
end
end
context "when logged in as a moderator" do
before { sign_in(moderator) }
include_examples "email handling not allowed"
end
context "when logged in as a non-staff user" do
before { sign_in(user) }
include_examples "email handling not allowed"
end
end
describe "#advanced_test" do
let(:email) { <<~EMAIL }
From: "somebody" <somebody@example.com>
To: someone@example.com
Date: Mon, 3 Dec 2018 00:00:00 -0000
Subject: This is some subject
Content-Type: text/plain; charset="UTF-8"
Hello, this is a test!
---
This part should be elided.
EMAIL
context "when logged in as an admin" do
before { sign_in(admin) }
it "should ..." do
post "/admin/email/advanced-test.json", params: { email: email }
expect(response.status).to eq(200)
incoming = response.parsed_body
expect(incoming["format"]).to eq(1)
expect(incoming["text"]).to eq("Hello, this is a test!")
expect(incoming["elided"]).to eq("---\n\nThis part should be elided.")
end
end
shared_examples "advanced email tests not allowed" do
it "prevents advanced email tests with a 404 response" do
post "/admin/email/advanced-test.json", params: { email: email }
expect(response.status).to eq(404)
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
end
end
context "when logged in as a moderator" do
before { sign_in(moderator) }
include_examples "advanced email tests not allowed"
end
context "when logged in as a non-staff user" do
before { sign_in(user) }
include_examples "advanced email tests not allowed"
end
end
end