0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-05 21:57:36 +08:00
discourse/spec/requests/webhooks_controller_spec.rb
Nat 3a3d315a85 SECURITY: Prevent any signed AWS SNS TopicARN from being accepted via webhooks [backport 2026.1]
Manual backport of #732 to 2026.1. Conflict in server.en.yml (no qq_mail_smtp line on this branch) resolved.
  GHSA-8f9m-v436-wr3x

---

Any AWS account holder can subscribe their own SNS topic to a Discourse instance's /webhooks/aws and publish bounce notifications that AWS will sign legitimately. The forged bounces are processed against arbitrary user emails, bumping bounce_score and eventually triggering email revocation..

This fix adds a new `aws_sns_topic_arn_allowlist` site setting. Also hardens Jobs::ProcessSnsNotification against three issues:
- Binds bounces to (message_id, to_address) via find_by, so a legitimately-subscribed SNS publisher can no longer bounce arbitrary recipients we didn't send to.
- Skips duplicate notifications (next if email_log.bounced?) — AWS SNS delivers at-least-once.
- Uses update! instead of update_columns so EmailLog's existing before_save normalizes the bounce status code.

Also add a dashboard problem flags self-hosted admins whose SMTP looks like SES but who haven't set the allowlist yet.

https://github.com/discourse/discourse/security/advisories/GHSA-8f9m-v436-wr3x
2026-06-30 16:27:06 +02:00

893 lines
31 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe WebhooksController do
before { Discourse.redis.flushdb }
fab!(:email) { "em@il.com" }
fab!(:message_id) { "12345@il.com" }
describe "#mailgun" do
let(:token) { "705a8ccd2ce932be8e98c221fe701c1b4a0afcb8bbd57726de" }
let(:timestamp) { Time.now.to_i }
let(:data) { "#{timestamp}#{token}" }
let(:signature) { OpenSSL::HMAC.hexdigest("SHA256", SiteSetting.mailgun_api_key, data) }
before do
SiteSetting.mailgun_api_key = "key-8221462f0c915af3f6f2e2df7aa5a493"
ActionController::Base.allow_forgery_protection = true # Ensure the endpoint works, even with CSRF protection generally enabled
end
after { ActionController::Base.allow_forgery_protection = false }
it "works (deprecated)" do
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/mailgun.json",
params: {
"token" => token,
"timestamp" => timestamp,
"event" => "dropped",
"recipient" => email,
"Message-Id" => "<#{message_id}>",
"signature" => signature,
"error" => "smtp; 550-5.1.1 The email account that you tried to reach does not exist.",
"code" => "5.1.1",
}
expect(response.status).to eq(200)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.bounce_error_code).to eq("5.1.1")
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
end
it "works (new)" do
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/mailgun.json",
params: {
"signature" => {
"token" => token,
"timestamp" => timestamp,
"signature" => signature,
},
"event-data" => {
"event" => "failed",
"severity" => "temporary",
"recipient" => email,
"message" => {
"headers" => {
"message-id" => message_id,
},
},
},
"delivery-status" => {
"message" =>
"smtp; 550-5.1.1 The email account that you tried to reach does not exist.",
"code" => "5.1.1",
"description" => "",
},
}
expect(response.status).to eq(200)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.bounce_error_code).to eq("5.1.1")
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.soft_bounce_score)
end
context "when readonly mode is enabled" do
before { Discourse.enable_readonly_mode }
it "returns 503" do
user = Fabricate(:user, email:)
email_log = Fabricate(:email_log, user:, message_id:, to_address: email)
post "/webhooks/mailgun.json",
params: {
"token" => token,
"timestamp" => timestamp,
"event" => "dropped",
"recipient" => email,
"Message-Id" => "<#{message_id}>",
"signature" => signature,
"error" =>
"smtp; 550-5.1.1 The email account that you tried to reach does not exist.",
"code" => "5.1.1",
}
expect(response.status).to eq(503)
expect(email_log.reload.bounced).to eq(false)
end
end
end
describe "#sendgrid" do
fab!(:user) { Fabricate(:user, email:) }
fab!(:email_log) { Fabricate(:email_log, user:, message_id: message_id, to_address: email) }
it "returns an error when no verification key is configured" do
SiteSetting.sendgrid_verification_key = ""
post "/webhooks/sendgrid.json",
params: {
"_json" => [
{
"email" => email,
"smtp-id" => "<12345@il.com>",
"event" => "bounce",
"status" => "5.0.0",
},
],
}
expect(response.status).to eq(406)
expect(email_log.reload.bounced).to eq(false)
end
it "hard bounces" do
SiteSetting.sendgrid_verification_key = "test"
WebhooksController.any_instance.stubs(:valid_sendgrid_signature?).returns(true)
post "/webhooks/sendgrid.json",
params: {
"_json" => [
{
"email" => email,
"smtp-id" => "<12345@il.com>",
"event" => "bounce",
"status" => "5.0.0",
},
],
}
expect(response.status).to eq(200)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.bounce_error_code).to eq("5.0.0")
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
end
it "sets the bounce error code to 5.1.2 when payload's `event` is `bounce`, `type` is `blocked` and `status` is blank" do
SiteSetting.sendgrid_verification_key = "test"
WebhooksController.any_instance.stubs(:valid_sendgrid_signature?).returns(true)
post "/webhooks/sendgrid.json",
params: {
"_json" => [
{
"email" => email,
"smtp-id" => "<12345@il.com>",
"event" => "bounce",
"type" => "blocked",
},
],
}
expect(response.status).to eq(200)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.bounce_error_code).to eq("5.1.2")
end
it "verifies signatures" do
SiteSetting.sendgrid_verification_key =
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE83T4O/n84iotIvIW4mdBgQ/7dAfSmpqIM8kF9mN1flpVKS3GRqe62gw+2fNNRaINXvVpiglSI8eNEc6wEA3F+g=="
post "/webhooks/sendgrid.json",
headers: {
"X-Twilio-Email-Event-Webhook-Signature" =>
"MEUCIGHQVtGj+Y3LkG9fLcxf3qfI10QysgDWmMOVmxG0u6ZUAiEAyBiXDWzM+uOe5W0JuG+luQAbPIqHh89M15TluLtEZtM=",
"X-Twilio-Email-Event-Webhook-Timestamp" => "1600112502",
},
params:
"[{\"email\":\"hello@world.com\",\"event\":\"dropped\",\"reason\":\"Bounced Address\",\"sg_event_id\":\"ZHJvcC0xMDk5NDkxOS1MUnpYbF9OSFN0T0doUTRrb2ZTbV9BLTA\",\"sg_message_id\":\"LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0\",\"smtp-id\":\"<LRzXl_NHStOGhQ4kofSm_A@ismtpd0039p1iad1.sendgrid.net>\",\"timestamp\":1600112492}]\r\n"
expect(response.status).to eq(200)
end
it "returns error if signature verification fails" do
SiteSetting.sendgrid_verification_key =
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE83T4O/n84iotIvIW4mdBgQ/7dAfSmpqIM8kF9mN1flpVKS3GRqe62gw+2fNNRaINXvVpiglSI8eNEc6wEA3F+g=="
post "/webhooks/sendgrid.json",
headers: {
"X-Twilio-Email-Event-Webhook-Signature" =>
"MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH3j/0=",
"X-Twilio-Email-Event-Webhook-Timestamp" => "1600112502",
},
params:
"[{\"email\":\"hello@world.com\",\"event\":\"dropped\",\"reason\":\"Bounced Address\",\"sg_event_id\":\"ZHJvcC0xMDk5NDkxOS1MUnpYbF9OSFN0T0doUTRrb2ZTbV9BLTA\",\"sg_message_id\":\"LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0\",\"smtp-id\":\"<LRzXl_NHStOGhQ4kofSm_A@ismtpd0039p1iad1.sendgrid.net>\",\"timestamp\":1600112492}]\r\n"
expect(response.status).to eq(406)
end
it "returns error if signature is invalid" do
SiteSetting.sendgrid_verification_key = "foo"
post "/webhooks/sendgrid.json",
headers: {
"X-Twilio-Email-Event-Webhook-Signature" =>
"MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH3j/0=",
"X-Twilio-Email-Event-Webhook-Timestamp" => "1600112502",
},
params:
"[{\"email\":\"hello@world.com\",\"event\":\"dropped\",\"reason\":\"Bounced Address\",\"sg_event_id\":\"ZHJvcC0xMDk5NDkxOS1MUnpYbF9OSFN0T0doUTRrb2ZTbV9BLTA\",\"sg_message_id\":\"LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0\",\"smtp-id\":\"<LRzXl_NHStOGhQ4kofSm_A@ismtpd0039p1iad1.sendgrid.net>\",\"timestamp\":1600112492}]\r\n"
expect(response.status).to eq(406)
end
end
describe "#mailjet" do
it "returns an error when no webhook token is configured" do
SiteSetting.mailjet_webhook_token = ""
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/mailjet.json",
params: {
"event" => "bounce",
"email" => email,
"hard_bounce" => true,
"CustomID" => message_id,
}
expect(response.status).to eq(406)
expect(email_log.reload.bounced).to eq(false)
end
it "hard bounces" do
SiteSetting.mailjet_webhook_token = "foo"
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/mailjet.json?t=foo",
params: {
"event" => "bounce",
"email" => email,
"hard_bounce" => true,
"CustomID" => message_id,
}
expect(response.status).to eq(200)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.bounce_error_code).to eq(nil) # mailjet doesn't give us this
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
end
it "verifies signatures" do
SiteSetting.mailjet_webhook_token = "foo"
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/mailjet.json?t=foo",
params: {
"event" => "bounce",
"email" => email,
"hard_bounce" => true,
"CustomID" => message_id,
}
expect(response.status).to eq(200)
expect(email_log.reload.bounced).to eq(true)
end
it "returns error if signature verification fails" do
SiteSetting.mailjet_webhook_token = "foo"
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/mailjet.json?t=bar",
params: {
"event" => "bounce",
"email" => email,
"hard_bounce" => true,
"CustomID" => message_id,
}
expect(response.status).to eq(406)
expect(email_log.reload.bounced).to eq(false)
end
end
describe "#mailpace" do
it "hard bounces" do
SiteSetting.mailpace_webhook_token = "foo"
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/mailpace.json?t=foo",
params: {
event: "email.bounced",
payload: {
status: "bounced",
to: email,
message_id: "<#{message_id}>",
},
}
expect(response.status).to eq(200)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.bounce_error_code).to eq(nil) # mailpace doesn't give us this
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
end
it "soft bounces" do
SiteSetting.mailpace_webhook_token = "foo"
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/mailpace.json?t=foo",
params: {
event: "email.deferred",
payload: {
status: "deferred",
to: email,
message_id: "<#{message_id}>",
},
}
expect(response.status).to eq(200)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.bounce_error_code).to eq(nil) # mailpace doesn't give us this
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.soft_bounce_score)
end
it "verifies webhook tokens" do
SiteSetting.mailpace_webhook_token = "foo"
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/mailpace.json?t=foo",
params: {
event: "email.bounced",
payload: {
status: "bounced",
to: email,
message_id: "<#{message_id}>",
},
}
expect(response.status).to eq(200)
expect(email_log.reload.bounced).to eq(true)
end
it "returns error if token verification fails" do
SiteSetting.mailpace_webhook_token = "foo"
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/mailpace.json?t=bar",
params: {
event: "email.bounced",
payload: {
status: "bounced",
to: email,
message_id: "<#{message_id}>",
},
}
expect(response.status).to eq(406)
expect(email_log.reload.bounced).to eq(false)
end
end
describe "#mandrill" do
it "returns an error when no authentication key is configured" do
SiteSetting.mandrill_authentication_key = ""
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/mandrill.json",
params: {
mandrill_events: [
{
"event" => "hard_bounce",
"msg" => {
"email" => email,
"diag" => "5.1.1",
"metadata" => {
"message_id" => message_id,
},
},
},
].to_json,
}
expect(response.status).to eq(406)
expect(email_log.reload.bounced).to eq(false)
end
let(:payload) do
"mandrill_events=%5B%7B%22event%22%3A%22hard_bounce%22%2C%22msg%22%3A%7B%22email%22%3A%22em%40il.com%22%2C%22diag%22%3A%225.1.1%22%2C%22bounce_description%22%3A%22smtp%3B+550-5.1.1+The+email+account+that+you+tried+to+reach+does+not+exist.%22%2C%22metadata%22%3A%7B%22message_id%22%3A%2212345%40il.com%22%7D%7D%7D%5D"
end
it "hard bounces" do
SiteSetting.mandrill_authentication_key = "test"
WebhooksController.any_instance.stubs(:valid_mandrill_signature?).returns(true)
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/mandrill.json",
params: {
mandrill_events: [
{
"event" => "hard_bounce",
"msg" => {
"email" => email,
"diag" => "5.1.1",
:"bounce_description" =>
"smtp; 550-5.1.1 The email account that you tried to reach does not exist.",
"metadata" => {
"message_id" => message_id,
},
},
},
].to_json,
}
expect(response.status).to eq(200)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.bounce_error_code).to eq("5.1.1")
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
end
it "verifies signatures" do
SiteSetting.mandrill_authentication_key = "wr_JeJNO9OI65RFDrvk3Zw"
post "/webhooks/mandrill.json",
headers: {
"X-Mandrill-Signature" => "Q5pCb903EjEqRZ99gZrlYKOfvIU=",
},
params: payload
expect(response.status).to eq(200)
end
it "returns error if signature verification fails" do
SiteSetting.mandrill_authentication_key = "wr_JeJNO9OI65RFDrvk3Zw"
post "/webhooks/mandrill.json", headers: { "X-Mandrill-Signature" => "foo" }, params: payload
expect(response.status).to eq(406)
end
it "returns error if signature is invalid" do
SiteSetting.mandrill_authentication_key = "foo"
post "/webhooks/mandrill.json",
headers: {
"X-Mandrill-Signature" => "Q5pCb903EjEqRZ99gZrlYKOfvIU=",
},
params: payload
expect(response.status).to eq(406)
end
end
describe "#mandrill_head" do
it "works" do
head "/webhooks/mandrill.json"
expect(response.status).to eq(200)
end
end
describe "#postmark" do
it "returns an error when no webhook token is configured" do
SiteSetting.postmark_webhook_token = ""
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/postmark.json",
params: {
"Type" => "HardBounce",
"MessageID" => message_id,
"Email" => email,
}
expect(response.status).to eq(406)
expect(email_log.reload.bounced).to eq(false)
end
it "hard bounces" do
SiteSetting.postmark_webhook_token = "foo"
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/postmark.json?t=foo",
params: {
"Type" => "HardBounce",
"MessageID" => message_id,
"Email" => email,
}
expect(response.status).to eq(200)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.bounce_error_code).to eq(nil) # postmark doesn't give us this
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
end
it "soft bounces" do
SiteSetting.postmark_webhook_token = "foo"
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/postmark.json?t=foo",
params: {
"Type" => "SoftBounce",
"MessageID" => message_id,
"Email" => email,
}
expect(response.status).to eq(200)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.bounce_error_code).to eq(nil) # postmark doesn't give us this
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.soft_bounce_score)
end
it "verifies signatures" do
SiteSetting.postmark_webhook_token = "foo"
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/postmark.json?t=foo",
params: {
"Type" => "HardBounce",
"MessageID" => message_id,
"Email" => email,
}
expect(response.status).to eq(200)
expect(email_log.reload.bounced).to eq(true)
end
it "returns error if signature verification fails" do
SiteSetting.postmark_webhook_token = "foo"
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/postmark.json?t=bar",
params: {
"Type" => "HardBounce",
"MessageID" => message_id,
"Email" => email,
}
expect(response.status).to eq(406)
expect(email_log.reload.bounced).to eq(false)
end
end
describe "#sparkpost" do
it "returns an error when no webhook token is configured" do
SiteSetting.sparkpost_webhook_token = ""
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/sparkpost.json",
params: {
"_json" => [
{
"msys" => {
"message_event" => {
"bounce_class" => 10,
"error_code" => "554",
"rcpt_to" => email,
"rcpt_meta" => {
"message_id" => message_id,
},
},
},
},
],
}
expect(response.status).to eq(406)
expect(email_log.reload.bounced).to eq(false)
end
it "hard bounces" do
SiteSetting.sparkpost_webhook_token = "foo"
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/sparkpost.json?t=foo",
params: {
"_json" => [
{
"msys" => {
"message_event" => {
"bounce_class" => 10,
"error_code" => "554",
"rcpt_to" => email,
"rcpt_meta" => {
"message_id" => message_id,
},
},
},
},
],
}
expect(response.status).to eq(200)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
end
it "verifies signatures" do
SiteSetting.sparkpost_webhook_token = "foo"
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/sparkpost.json?t=foo",
params: {
"_json" => [
{
"msys" => {
"message_event" => {
"bounce_class" => 10,
"error_code" => "554",
"rcpt_to" => email,
"rcpt_meta" => {
"message_id" => message_id,
},
},
},
},
],
}
expect(response.status).to eq(200)
expect(email_log.reload.bounced).to eq(true)
end
it "returns error if signature verification fails" do
SiteSetting.sparkpost_webhook_token = "foo"
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/sparkpost.json?t=bar",
params: {
"_json" => [
{
"msys" => {
"message_event" => {
"bounce_class" => 10,
"error_code" => "554",
"rcpt_to" => email,
"rcpt_meta" => {
"message_id" => message_id,
},
},
},
},
],
}
expect(response.status).to eq(406)
expect(email_log.reload.bounced).to eq(false)
end
end
describe "#aws" do
let(:topic_arn) { "arn:aws:sns:us-east-1:123456789012:discourse-bounces" }
let(:other_topic_arn) { "arn:aws:sns:us-east-1:999999999999:attacker-topic" }
let(:bounce_status) { "5.1.1" }
let(:payload) do
{
"Type" => "Notification",
"TopicArn" => topic_arn,
"Message" => {
"notificationType" => "Bounce",
:"bounce" => {
"bounceType" => "Permanent",
"reportingMTA" => "dns; email.example.com",
:"bouncedRecipients" => [
{
"emailAddress" => email,
"status" => bounce_status,
"action" => "failed",
"diagnosticCode" => "smtp; 550 5.1.1 <#{email}>... User",
},
],
"bounceSubType" => "General",
"timestamp" => "2016-01-27T14:59:38.237Z",
"feedbackId" => "00000138111222aa-33322211-cccc-cccc-cccc-ddddaaaa068a-000000",
"remoteMtaIp" => "127.0.2.0",
},
:"mail" => {
"timestamp" => "2016-01-27T14:59:38.237Z",
"source" => "john@example.com",
"sourceArn" => "arn:aws:ses:us-east-1:888888888888:identity/example.com",
"sourceIp" => "127.0.3.0",
"sendingAccountId" => "123456789012",
"callerIdentity" => "IAM_user_or_role_name",
"messageId" => message_id,
"destination" => [email, "jane@example.com", "mary@example.com", "richard@example.com"],
"headersTruncated" => false,
"headers" => [
{ "name" => "From", "value" => "\"John Doe\" <john@example.com>" },
{
"name" => "To",
"value" =>
"\"Test\" <#{email}>, \"Jane Doe\" <jane@example.com>, \"Mary Doe\" <mary@example.com>, \"Richard Doe\" <richard@example.com>",
},
{ "name" => "Message-ID", "value" => message_id },
{ "name" => "Subject", "value" => "Hello" },
{ "name" => "Content-Type", "value" => "text/plain; charset=\"UTF-8\"" },
{ "name" => "Content-Transfer-Encoding", "value" => "base64" },
{ "name" => "Date", "value" => "Wed, 27 Jan 2016 14:05:45 +0000" },
],
"commonHeaders" => {
"from" => ["John Doe <john@example.com>"],
"date" => "Wed, 27 Jan 2016 14:05:45 +0000",
"to" => [
"\"Test\" <#{email}>, Jane Doe <jane@example.com>, Mary Doe <mary@example.com>, Richard Doe <richard@example.com>",
],
"messageId" => message_id,
"subject" => "Hello",
},
},
}.to_json,
}.to_json
end
let(:subscription_confirmation_payload) do
{
"Type" => "SubscriptionConfirmation",
"TopicArn" => topic_arn,
"Token" => "abc123",
"Message" => "You have chosen to subscribe to the topic #{topic_arn}",
"SubscribeURL" =>
"https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=#{topic_arn}&Token=abc123",
}.to_json
end
before do
Jobs.run_immediately!
require "aws-sdk-sns"
Aws::SNS::MessageVerifier.any_instance.stubs(:authentic?).returns(true)
SiteSetting.aws_sns_topic_arn_allowlist = topic_arn
end
it "hard bounces" do
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/aws.json", headers: { "RAW_POST_DATA" => payload }
expect(response.status).to eq(200)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.bounce_error_code).to eq("5.1.1")
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
end
it "does not bounce an email log with a different SES message id" do
user = Fabricate(:user, email: email)
email_log =
Fabricate(:email_log, user: user, message_id: "other-message-id", to_address: email)
post "/webhooks/aws.json", headers: { "RAW_POST_DATA" => payload }
expect(response.status).to eq(200)
expect(email_log.reload.bounced).to eq(false)
expect(email_log.user.user_stat.bounce_score).to eq(0)
end
it "does not increase the bounce score for duplicate notifications" do
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/aws.json", headers: { "RAW_POST_DATA" => payload }
expect(response.status).to eq(200)
post "/webhooks/aws.json", headers: { "RAW_POST_DATA" => payload }
expect(response.status).to eq(200)
expect(email_log.reload.bounced).to eq(true)
expect(email_log.user.user_stat.reload.bounce_score).to eq(SiteSetting.hard_bounce_score)
end
context "with a non-normalized bounce status" do
let(:bounce_status) { "5.0.0 (permanent failure)" }
it "stores the normalized bounce error code" do
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/aws.json", headers: { "RAW_POST_DATA" => payload }
expect(response.status).to eq(200)
expect(email_log.reload.bounce_error_code).to eq("5.0.0")
end
end
context "when the allowlist is empty" do
before { SiteSetting.aws_sns_topic_arn_allowlist = "" }
it "rejects notifications with 406 and does not process the bounce" do
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/aws.json", headers: { "RAW_POST_DATA" => payload }
expect(response.status).to eq(406)
expect(email_log.reload.bounced).to eq(false)
expect(email_log.user.user_stat.bounce_score).to eq(0)
end
end
context "when the TopicArn is not on the allowlist" do
let(:payload_with_other_topic) do
parsed = JSON.parse(payload)
parsed["TopicArn"] = other_topic_arn
parsed.to_json
end
it "rejects with 406 and does not process the bounce" do
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/aws.json", headers: { "RAW_POST_DATA" => payload_with_other_topic }
expect(response.status).to eq(406)
expect(email_log.reload.bounced).to eq(false)
expect(email_log.user.user_stat.bounce_score).to eq(0)
end
end
context "when the SNS signature is invalid" do
before { Aws::SNS::MessageVerifier.any_instance.stubs(:authentic?).returns(false) }
it "rejects with 406" do
user = Fabricate(:user, email: email)
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
post "/webhooks/aws.json", headers: { "RAW_POST_DATA" => payload }
expect(response.status).to eq(406)
expect(email_log.reload.bounced).to eq(false)
end
end
context "with a SubscriptionConfirmation" do
before { Net::HTTP.stubs(:get).returns("") }
it "enqueues the confirmation job when the TopicArn is allowlisted" do
Jobs::ConfirmSnsSubscription.any_instance.expects(:execute).once
post "/webhooks/aws.json", headers: { "RAW_POST_DATA" => subscription_confirmation_payload }
expect(response.status).to eq(200)
end
it "rejects with 406 when the TopicArn is not allowlisted" do
Jobs::ConfirmSnsSubscription.any_instance.expects(:execute).never
attacker_payload =
JSON.parse(subscription_confirmation_payload).merge("TopicArn" => other_topic_arn).to_json
post "/webhooks/aws.json", headers: { "RAW_POST_DATA" => attacker_payload }
expect(response.status).to eq(406)
end
end
end
end