Skip to content

Commit

Permalink
fix: add webhook signature and clean up inquiry payload checks
Browse files Browse the repository at this point in the history
  • Loading branch information
shaunwarman committed Aug 22, 2024
1 parent 0de1c1d commit 908d781
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 32 deletions.
8 changes: 8 additions & 0 deletions .env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,14 @@ HELPER_ENCRYPTION_KEY_LEGACY=
# generate with `crypto.randomBytes(16).toString('hex')`
TXT_ENCRYPTION_KEY=

###########################
## Webhook Signature Key ##
###########################
# SHA256 HMAC should not exceed 512 bytes for key length
# <https://security.stackexchange.com/a/96176>
# randomBytes(16).toString('hex')
WEBHOOK_SIGNATURE_KEY=

###########################
## api restricted symbol ##
###########################
Expand Down
8 changes: 8 additions & 0 deletions .env.schema
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,14 @@ HELPER_ENCRYPTION_KEY_LEGACY=
# generate with `crypto.randomBytes(16).toString('hex')`
TXT_ENCRYPTION_KEY=

###########################
## Webhook Signature Key ##
###########################
# SHA256 HMAC should not exceed 512 bytes for key length
# <https://security.stackexchange.com/a/96176>
# randomBytes(16).toString('hex')
WEBHOOK_SIGNATURE_KEY=

###########################
## api restricted symbol ##
###########################
Expand Down
80 changes: 48 additions & 32 deletions app/controllers/api/v1/inquiries.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@
* SPDX-License-Identifier: BUSL-1.1
*/

const { createHmac } = require('node:crypto');
const process = require('node:process');
const Boom = require('@hapi/boom');
const isSANB = require('is-string-and-not-blank');
const { isEmail } = require('validator');
const { simpleParser } = require('mailparser');
const { Headers } = require('mailsplit');

const { decrypt } = require('./encrypt-decrypt');
const config = require('#config');
const env = require('#config/env');
const { Inquiries, Users } = require('#models');

const webhookSignatureKey = process.env.WEBHOOK_SIGNATURE_KEY;

function findHeaderByName(name, headers) {
for (const header of headers) {
const key = header.key.toLowerCase();
Expand All @@ -25,12 +30,28 @@ function findHeaderByName(name, headers) {

// eslint-disable-next-line complexity
async function create(ctx) {
const { body } = ctx.request;
const { body, headers: requestHeaders } = ctx.request;

ctx.logger.info('creating inquiry from webhook');

// TODO: Add support for webhook payload signature:
// https://stackoverflow.com/questions/68885086/how-to-create-signed-webhook-requests-in-nodejs/68885281#68885281
if (!requestHeaders['X-Webhook-Signature']) {
return ctx.throw(
Boom.badRequest(
ctx.translateError('MISSING_INQUIRY_WEBHOOK_SIGNATURE_HEADER')
)
);
}

const webhookSignature = createHmac('sha256', decrypt(webhookSignatureKey))
.update(body)
.digest('hex');

if (requestHeaders['X-Webhook-Signature'] !== webhookSignature) {
return ctx.throw(
Boom.forbidden(ctx.translateError('INVALID_INQUIRY_WEBHOOK_SIGNATURE'))
);
}

if (
!ctx.allowlistValue ||
![env.MX1_HOST, env.MX2_HOST, env.WEB_HOST].includes(ctx.allowlistValue)
Expand All @@ -39,15 +60,10 @@ async function create(ctx) {
Boom.forbidden(ctx.translateError('INVALID_INQUIRY_WEBHOOK_REQUEST'))
);

let parsed;
try {
parsed = await simpleParser(body.raw);
} catch (err) {
ctx.logger.error(err);
return ctx.throw(err);
}

const { headerLines, session, text } = body;

const headers = new Headers(headerLines);

if (!session)
return ctx.throw(
Boom.badRequest(ctx.translateError('INVALID_INQUIRY_WEBHOOK_PAYLOAD'))
Expand All @@ -59,29 +75,29 @@ async function create(ctx) {
);

if (
(parsed.headers.has('Auto-submitted') &&
parsed.headers.get('Auto-submitted') !== 'no') ||
(parsed.headers.has('Auto-Submitted') &&
parsed.headers.get('Auto-Submitted') !== 'no') ||
(parsed.headers.has('X-Auto-Response-Suppress') &&
(headers.hasHeader('Auto-submitted') &&
headers.getFirst('Auto-submitted') !== 'no') ||
(headers.hasHeader('Auto-Submitted') &&
headers.getFirst('Auto-Submitted') !== 'no') ||
(headers.hasHeader('X-Auto-Response-Suppress') &&
['dr', 'autoreply', 'auto-reply', 'auto_reply', 'all'].includes(
parsed.headers.get('X-Auto-Response-Suppress').toLowerCase().trim()
headers.getFirst('X-Auto-Response-Suppress').toLowerCase().trim()
)) ||
parsed.headers.has('List-Id') ||
parsed.headers.has('List-id') ||
parsed.headers.has('List-Unsubscribe') ||
parsed.headers.has('List-unsubscribe') ||
parsed.headers.has('Feedback-ID') ||
parsed.headers.has('Feedback-Id') ||
parsed.headers.has('X-Autoreply') ||
parsed.headers.has('X-Auto-Reply') ||
parsed.headers.has('X-AutoReply') ||
parsed.headers.has('X-Autorespond') ||
parsed.headers.has('X-Auto-Respond') ||
parsed.headers.has('X-AutoRespond') ||
(parsed.headers.has('Precedence') &&
headers.hasHeader('List-Id') ||
headers.hasHeader('List-id') ||
headers.hasHeader('List-Unsubscribe') ||
headers.hasHeader('List-unsubscribe') ||
headers.hasHeader('Feedback-ID') ||
headers.hasHeader('Feedback-Id') ||
headers.hasHeader('X-Autoreply') ||
headers.hasHeader('X-Auto-Reply') ||
headers.hasHeader('X-AutoReply') ||
headers.hasHeader('X-Autorespond') ||
headers.hasHeader('X-Auto-Respond') ||
headers.hasHeader('X-AutoRespond') ||
(headers.hasHeader('Precedence') &&
['bulk', 'autoreply', 'auto-reply', 'auto_reply', 'list'].includes(
parsed.headers.get('Precedence').toLowerCase().trim()
headers.getFirst('Precedence').toLowerCase().trim()
))
)
return;
Expand Down
3 changes: 3 additions & 0 deletions config/phrases.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ module.exports = {
INVALID_INQUIRY_WEBHOOK_PAYLOAD: 'Invalid inquiry webhook payload.',
INVALID_INQUIRY_WEBHOOK_REQUEST:
'Webhook request did not originate from a valid hostname',
MISSING_INQUIRY_WEBHOOK_SIGNATURE_HEADER:
'Webhook request missing X-Signature-Header',
INVALID_INQUIRY_WEBHOOK_SIGNATURE: 'Invalid signature in webhook request',
INVALID_USER: 'User does not exist.',
INVALID_LOG: 'Log does not exist.',
INVALID_MEMBER: 'Member does not exist.',
Expand Down

0 comments on commit 908d781

Please sign in to comment.