fix #251: verify signatures of SNS messages

This commit is contained in:
Luke Crouch 2018-08-23 11:43:57 -05:00
Родитель dd49eddb0b
Коммит 5deb41a54e
10 изменённых файлов: 2258 добавлений и 1803 удалений

Просмотреть файл

@ -19,9 +19,6 @@ SMTP_URL=""
EMAIL_FROM=""
# https://docs.aws.amazon.com/ses/latest/DeveloperGuide/using-configuration-sets.html
SES_CONFIG_SET=""
# username and password to authenticate messages coming back from SES
SES_USERNAME=""
SES_PASSWORD=""
# 1: only log messages coming back from SES
SES_NOTIFICATION_LOG_ONLY=

Просмотреть файл

@ -14,8 +14,6 @@ const kEnvironmentVariables = [
"SMTP_URL",
"EMAIL_FROM",
"SES_CONFIG_SET",
"SES_USERNAME",
"SES_PASSWORD",
"SES_NOTIFICATION_LOG_ONLY",
"OAUTH_AUTHORIZATION_URI",
"OAUTH_TOKEN_URI",

Просмотреть файл

@ -1,23 +1,32 @@
"use strict";
const MessageValidator = require("sns-validator");
const DB = require("../db/DB");
async function notification(req, res) {
try {
const notification = JSON.parse(req.body);
// TODO: verifyNotification(notification) or use http basic auth
await handleNotification(notification);
const validator = new MessageValidator();
res.status(200).json(
{status: "OK"}
);
} catch (e) {
console.error("SES notification error: ", e);
res.status(500).json(
{info: "Internal error."}
);
}
async function notification(req, res) {
const message = JSON.parse(req.body);
return new Promise((resolve, reject) => {
validator.validate(message, async (err, message) => {
if (err) {
console.error(err);
const body = "Access denied. " + err.message;
res.status(401).send(body);
return reject(body);
}
await handleNotification(message);
res.status(200).json(
{status: "OK"}
);
return resolve("OK");
});
});
}

Просмотреть файл

@ -1,25 +1,5 @@
"use strict";
const auth = require("basic-auth");
const AppConstants = require("./app-constants");
function checkSESAuth (user, pass) {
return user === AppConstants.SES_USERNAME && pass === AppConstants.SES_PASSWORD;
}
function sesAuth (req, res, next) {
const credentials = auth(req);
if (!credentials || !checkSESAuth(credentials.name, credentials.pass)) {
res.setHeader("WWW-Authenticate", `Basic realm="${AppConstants.SERVER_URL}"`);
res.status(401).send("Access denied");
} else {
next();
}
}
// Helps handle errors for all async route controllers
// See https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
@ -30,6 +10,5 @@ function asyncMiddleware (fn) {
}
module.exports = {
sesAuth,
asyncMiddleware,
};

3944
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -27,6 +27,7 @@
"nodemailer": "^4.6.4",
"nodemailer-express-handlebars": "^3.0.0",
"pg": "^7.4.3",
"sns-validator": "^0.3.4",
"uuid": "^3.2.1"
},
"devDependencies": {
@ -38,6 +39,7 @@
"jest": "^23.5.0",
"jest-tap-reporter": "^1.9.0",
"node-mocks-http": "^1.7.0",
"nodemon": "^1.18.3",
"npm-run-all": "^4.1.3",
"nsp": "^3.2.1",
"stylelint": "^9.2.0",
@ -49,6 +51,11 @@
"homepage": "https://github.com/mozilla/blurts-server",
"license": "MPL-2.0",
"main": "server.js",
"nodemonConfig": {
"ignore": [
"version.json"
]
},
"jest": {
"collectCoverageFrom": [
"**/*.js",

Просмотреть файл

@ -4,7 +4,6 @@ const bodyParser = require("body-parser");
const express = require("express");
const AppConstants = require("../app-constants");
const {sesAuth} = require("../middleware");
const {notification} = require("../controllers/ses");
@ -16,7 +15,6 @@ if (AppConstants.SES_NOTIFICATION_LOG_ONLY) {
console.log("SES Notification request body: ", req.body);
});
} else {
router.use("/notification", sesAuth);
router.post("/notification", textParser, notification);
}

Просмотреть файл

@ -0,0 +1,12 @@
{
"Type" : "Notification",
"MessageId" : "a33de653-86a5-509e-a94f-b5b8670d343e",
"TopicArn" : "arn:aws:sns:eu-west-1:142069644989:Breach-Alerts-SNS-Bounce-Handler-Test-EU",
"Subject" : "Amazon SES Email Event Notification",
"Message" : "{\"eventType\":\"Complaint\",\"complaint\":{\"complainedRecipients\":[{\"emailAddress\":\"complaint@simulator.amazonses.com\"}],\"timestamp\":\"2018-08-13T19:43:36.073Z\",\"feedbackId\":\"0102016534d0ae14-2f329c45-acec-41eb-9bfb-fde35bdd6dfd-000000\",\"userAgent\":\"Amazon SES Mailbox Simulator\",\"complaintFeedbackType\":\"abuse\",\"arrivalDate\":\"2018-08-13T19:43:36.073Z\"},\"mail\":{\"timestamp\":\"2018-08-13T19:43:34.192Z\",\"source\":\"breach-alerts@mozilla.com\",\"sourceArn\":\"arn:aws:ses:eu-west-1:142069644989:identity/breach-alerts@mozilla.com\",\"sendingAccountId\":\"142069644989\",\"messageId\":\"0102016534d0a730-846fdbf2-14f0-4f28-8a94-e8f3a0ef5357-000000\",\"destination\":[\"complaint@simulator.amazonses.com\"],\"headersTruncated\":false,\"headers\":[{\"name\":\"Received\",\"value\":\"from [127.0.0.1] (ec2-54-195-19-74.eu-west-1.compute.amazonaws.com [54.195.19.74]) by email-smtp.amazonaws.com with SMTP (SimpleEmailService-2762311919) id bqzqHO8xSrImZmxP8QfY for complaint@simulator.amazonses.com; Mon, 13 Aug 2018 19:43:34 +0000 (UTC)\"},{\"name\":\"Content-Type\",\"value\":\"text/html\"},{\"name\":\"X-Ses-Configuration-Set\",\"value\":\"Breach-Alerts-Bounce-Handler-Test-EU\"},{\"name\":\"From\",\"value\":\"breach-alerts@mozilla.com\"},{\"name\":\"To\",\"value\":\"complaint@simulator.amazonses.com\"},{\"name\":\"Subject\",\"value\":\"Verify your email address to subscribe to Firefox Monitor.\"},{\"name\":\"Message-ID\",\"value\":\"<315963a8-05e7-88ab-2657-7841295f18fd@mozilla.com>\"},{\"name\":\"Content-Transfer-Encoding\",\"value\":\"quoted-printable\"},{\"name\":\"Date\",\"value\":\"Mon, 13 Aug 2018 19:43:34 +0000\"},{\"name\":\"MIME-Version\",\"value\":\"1.0\"}],\"commonHeaders\":{\"from\":[\"breach-alerts@mozilla.com\"],\"date\":\"Mon, 13 Aug 2018 19:43:34 +0000\",\"to\":[\"complaint@simulator.amazonses.com\"],\"messageId\":\"0102016534d0a730-846fdbf2-14f0-4f28-8a94-e8f3a0ef5357-000000\",\"subject\":\"Verify your email address to subscribe to Firefox Monitor.\"},\"tags\":{\"ses:operation\":[\"SendSmtpEmail\"],\"ses:configuration-set\":[\"Breach-Alerts-Bounce-Handler-Test-EU\"],\"ses:source-ip\":[\"54.195.19.74\"],\"ses:from-domain\":[\"mozilla.com\"],\"ses:caller-identity\":[\"groovecoder-breach-alerts-test\"]}}}\n",
"Timestamp" : "2018-08-13T19:43:36.158Z",
"SignatureVersion" : "1",
"Signature" : "f7SzPsZND/a3eGyaXRXcpXqP+L8z5eNIdB3K0kSlG+Sq4x+QylTut8Gvvg6QwSOcesuvHlVmmxdRMQWn1hndSAX1/qzQEASs8ousZUDs8CES3MHsJP9jlzZRZBYQ0354Mssn11jm40rN1qKBMdYjTq30ArGCYfipugLgvvNK+v9zPl4TrJkE5Y3vi+UN7AK0rAKty/udPOuZ/M8ghj4tlzGnC8HlOvj42be8CInY2WTAY/u2TdGhwfT29RtY7+zhS4FlNNDvs440M+4pfHhvo+U6XkWcTXw1oejjNNMh6RS4jkofB3vbiH0lMdV0do2MhQOWCYQ3Un6jIHfOpyQGQ==",
"SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-eaea6120e66ea12e88dcd8bcbddca752.pem",
"UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:142069644989:Breach-Alerts-SNS-Bounce-Handler-Test-EU:44888dbc-b2bc-4089-9133-f85be406d0d3"
}

Просмотреть файл

@ -1,12 +1,12 @@
{
"MessageId" : "73ed7828-153e-5e60-b776-2bd6480d2f7e",
"Type" : "Notification",
"MessageId" : "d53af48a-ce18-5709-9579-a7611a14918d",
"TopicArn" : "arn:aws:sns:eu-west-1:142069644989:Breach-Alerts-SNS-Bounce-Handler-Test-EU",
"Subject" : "Amazon SES Email Event Notification",
"Message" : "{\"eventType\":\"Bounce\",\"bounce\":{\"bounceType\":\"Permanent\",\"bounceSubType\":\"General\",\"bouncedRecipients\":[{\"emailAddress\":\"bounce@simulator.amazonses.com\",\"action\":\"failed\",\"status\":\"5.1.1\",\"diagnosticCode\":\"smtp; 550 5.1.1 user unknown\"}],\"timestamp\":\"2018-08-13T18:45:32.904Z\",\"feedbackId\":\"01020165349b87e6-6170ebb9-9d69-40b0-9b84-51214b7c89dd-000000\",\"reportingMTA\":\"dsn; a4-11.smtp-out.eu-west-1.amazonses.com\"},\"mail\":{\"timestamp\":\"2018-08-13T18:45:32.004Z\",\"source\":\"breach-alerts@mozilla.com\",\"sourceArn\":\"arn:aws:ses:eu-west-1:142069644989:identity/breach-alerts@mozilla.com\",\"sendingAccountId\":\"142069644989\",\"messageId\":\"01020165349b84e4-529247a4-4204-4c85-bd99-6b283bd85952-000000\",\"destination\":[\"bounce@simulator.amazonses.com\"],\"headersTruncated\":false,\"headers\":[{\"name\":\"Received\",\"value\":\"from [127.0.0.1] (ec2-54-195-19-74.eu-west-1.compute.amazonaws.com [54.195.19.74]) by email-smtp.amazonaws.com with SMTP (SimpleEmailService-2762311919) id Yd1vUB46cMuDE1VCjMl4 for bounce@simulator.amazonses.com; Mon, 13 Aug 2018 18:45:31 +0000 (UTC)\"},{\"name\":\"Content-Type\",\"value\":\"text/html\"},{\"name\":\"X-Ses-Configuration-Set\",\"value\":\"Breach-Alerts-Bounce-Handler-Test-EU\"},{\"name\":\"From\",\"value\":\"breach-alerts@mozilla.com\"},{\"name\":\"To\",\"value\":\"bounce@simulator.amazonses.com\"},{\"name\":\"Subject\",\"value\":\"Verify your email address to subscribe to Firefox Monitor.\"},{\"name\":\"Message-ID\",\"value\":\"<c15a7b7a-653e-afa9-375c-9bbfa959699a@mozilla.com>\"},{\"name\":\"Content-Transfer-Encoding\",\"value\":\"quoted-printable\"},{\"name\":\"Date\",\"value\":\"Mon, 13 Aug 2018 18:45:31 +0000\"},{\"name\":\"MIME-Version\",\"value\":\"1.0\"}],\"commonHeaders\":{\"from\":[\"breach-alerts@mozilla.com\"],\"date\":\"Mon, 13 Aug 2018 18:45:31 +0000\",\"to\":[\"bounce@simulator.amazonses.com\"],\"messageId\":\"01020165349b84e4-529247a4-4204-4c85-bd99-6b283bd85952-000000\",\"subject\":\"Verify your email address to subscribe to Firefox Monitor.\"},\"tags\":{\"ses:operation\":[\"SendSmtpEmail\"],\"ses:configuration-set\":[\"Breach-Alerts-Bounce-Handler-Test-EU\"],\"ses:source-ip\":[\"54.195.19.74\"],\"ses:from-domain\":[\"mozilla.com\"],\"ses:caller-identity\":[\"groovecoder-breach-alerts-test\"]}}}\n",
"Timestamp" : "2018-08-13T18:45:32.984Z",
"Message" : "{\"eventType\":\"Bounce\",\"bounce\":{\"bounceType\":\"Permanent\",\"bounceSubType\":\"General\",\"bouncedRecipients\":[{\"emailAddress\":\"bounce@simulator.amazonses.com\",\"action\":\"failed\",\"status\":\"5.1.1\",\"diagnosticCode\":\"smtp; 550 5.1.1 user unknown\"}],\"timestamp\":\"2018-08-27T20:11:31.327Z\",\"feedbackId\":\"010201657d034625-c8dfb425-9962-4104-b480-8cd1761aa963-000000\",\"reportingMTA\":\"dsn; a4-12.smtp-out.eu-west-1.amazonses.com\"},\"mail\":{\"timestamp\":\"2018-08-27T20:11:30.533Z\",\"source\":\"breach-alerts@mozilla.com\",\"sourceArn\":\"arn:aws:ses:eu-west-1:142069644989:identity/breach-alerts@mozilla.com\",\"sendingAccountId\":\"142069644989\",\"messageId\":\"010201657d034365-d2209285-9b0c-438e-b75d-109b59764ca9-000000\",\"destination\":[\"bounce@simulator.amazonses.com\"],\"headersTruncated\":false,\"headers\":[{\"name\":\"Received\",\"value\":\"from [127.0.0.1] (ec2-54-74-203-173.eu-west-1.compute.amazonaws.com [54.74.203.173]) by email-smtp.amazonaws.com with SMTP (SimpleEmailService-3236554995) id 8g3PpKxw1KFDvWCHWsUS for bounce@simulator.amazonses.com; Mon, 27 Aug 2018 20:11:30 +0000 (UTC)\"},{\"name\":\"Content-Type\",\"value\":\"text/html\"},{\"name\":\"X-Ses-Configuration-Set\",\"value\":\"Breach-Alerts-Bounce-Handler-Test-EU\"},{\"name\":\"From\",\"value\":\"breach-alerts@mozilla.com\"},{\"name\":\"To\",\"value\":\"bounce@simulator.amazonses.com\"},{\"name\":\"Subject\",\"value\":\"Verify your email address to subscribe to Firefox Monitor.\"},{\"name\":\"Message-ID\",\"value\":\"<ef207157-e80a-89f5-a2f1-441c99bafd29@mozilla.com>\"},{\"name\":\"Content-Transfer-Encoding\",\"value\":\"quoted-printable\"},{\"name\":\"Date\",\"value\":\"Mon, 27 Aug 2018 20:11:30 +0000\"},{\"name\":\"MIME-Version\",\"value\":\"1.0\"}],\"commonHeaders\":{\"from\":[\"breach-alerts@mozilla.com\"],\"date\":\"Mon, 27 Aug 2018 20:11:30 +0000\",\"to\":[\"bounce@simulator.amazonses.com\"],\"messageId\":\"010201657d034365-d2209285-9b0c-438e-b75d-109b59764ca9-000000\",\"subject\":\"Verify your email address to subscribe to Firefox Monitor.\"},\"tags\":{\"ses:operation\":[\"SendSmtpEmail\"],\"ses:configuration-set\":[\"Breach-Alerts-Bounce-Handler-Test-EU\"],\"ses:source-ip\":[\"54.74.203.173\"],\"ses:from-domain\":[\"mozilla.com\"],\"ses:caller-identity\":[\"groovecoder-breach-alerts-test\"]}}}\n",
"Timestamp" : "2018-08-27T20:11:31.445Z",
"SignatureVersion" : "1",
"Signature" : "F8teyD3keOj0bJr7dV4xVLzEUAfF90aEXbQp92Pg5tXrMldr1VQ3r6mDn/yl2nDT4pD3pOkCG9EGoqk2nvwv7lNiIZl87yxbKmSA/euFtbneuLB8R2Bp9ebCIYwc/xVRXEBZshtWLR8QFfB5W6TfxHndXBIMsqx68USPJMfngnMIHpIk7ztwJji6qN2Cy4dhImGkK6/sDRSDqM00iyHVNdheGxycQKyeN4vos8khlQVFwCxH0IsjrwAZBhtwwOuOLJ+OEC+FCKT6lMGpjhm4WuG9jrB6s6uQKfnl8nEjpga0Qyb0Smr1yLMsfUgAzdudAQ8hyyMlUCGP3xBJ3tPQHw==",
"Signature" : "qVG4+hbkFeJzkrsxsCUAjmmhv3GTeF4TKqPKBcN96L0BQbsfrZkvYv1uhpGwv4ocTZjLyEo/qNNQyq4fXNzEaqQQxGi4540hZ7S06gB8jiq79S6s9CvlZISJ9jVsiFrhFOQRQ6APzqJRAQRO0lWXCKx6+fwzIdF8OOrv6lHIdx9FQZDWsAMypURXftjTkihzS7BuIIayI3C1l0mPl7C6Vx9psYQAgTq7HFMYosVgKSPdlxoNI6vdpl6OHApM5Z5BpjCA7wfCcvikJJF3tQ7/JQLTMoNZD8n/WJf0FXqjDbg3gd8MtYqIhd5GaT1rKtQCC55B67EiaWTkprIOBECF/w==",
"SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-eaea6120e66ea12e88dcd8bcbddca752.pem",
"UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:142069644989:Breach-Alerts-SNS-Bounce-Handler-Test-EU:44888dbc-b2bc-4089-9133-f85be406d0d3"
"UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:142069644989:Breach-Alerts-SNS-Bounce-Handler-Test-EU:e1ccb7b4-4946-472c-8482-82ec7d65bb23"
}

Просмотреть файл

@ -12,6 +12,7 @@ require("./resetDB");
const testNotifications = new Map();
testNotifications.set("bounce", require("./ses-bounce-notification.json"));
testNotifications.set("complaint", require("./ses-complaint-notification.json"));
testNotifications.set("invalid", require("./invalid-signature-ses-complaint-notification.json"));
const createRequestBody = function(notificationType, bounceSubType = null) {
@ -71,3 +72,25 @@ test("ses notification with Complaint unsubscribes recipient", async () => {
subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]);
expect(subscribers.length).toEqual(0);
});
test("ses notification with invalid signature responds with error and doesn't change subscribers", async () => {
const testEmail = "complaint@simulator.amazonses.com";
await DB.addSubscriber(testEmail);
let subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]);
expect(subscribers.length).toEqual(1);
const req = httpMocks.createRequest({
method: "POST",
url: "/ses/notification",
body: createRequestBody("invalid"),
});
const resp = httpMocks.createResponse();
await expect(ses.notification(req, resp)).rejects.toMatch("invalid");
expect(resp.statusCode).toEqual(401);
subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]);
expect(subscribers.length).toEqual(1);
});