From 2eb5ea287d94c2657520863ada0ba41afae726ef Mon Sep 17 00:00:00 2001 From: Jason Levine Date: Sat, 1 Apr 2023 04:39:40 -0400 Subject: [PATCH] tighten Vapid subject validation (#789) only accept Vapid subjects with https: or mailto: URL protocols, as per spec add tests for localhost https: and mailto: subjects --- src/vapid-helper.js | 27 ++++++++++++++++----------- test/test-vapid-helper.js | 24 ++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/vapid-helper.js b/src/vapid-helper.js index 28d346f6..4c6cfe27 100644 --- a/src/vapid-helper.js +++ b/src/vapid-helper.js @@ -4,7 +4,7 @@ const crypto = require('crypto'); const urlBase64 = require('urlsafe-base64'); const asn1 = require('asn1.js'); const jws = require('jws'); -const url = require('url'); +const { URL } = require('url'); const WebPushConstants = require('./web-push-constants.js'); @@ -71,18 +71,22 @@ function validateSubject(subject) { } if (typeof subject !== 'string' || subject.length === 0) { - throw new Error('The subject value must be a string containing a URL or ' + throw new Error('The subject value must be a string containing an https: URL or ' + 'mailto: address. ' + subject); } - if (subject.indexOf('mailto:') !== 0) { - const subjectParseResult = url.parse(subject); - if (!subjectParseResult.hostname) { - throw new Error('Vapid subject is not a url or mailto url. ' + subject); - } else if (subjectParseResult.hostname === 'localhost' && subjectParseResult.protocol === 'https:') { - console.warn('VAPID subject points to a localhost web URI, which is unsupported by Apple\'s push notification ' - + 'server and will result in a BadJwtToken error when sending notifications.'); + try { + const subjectParseResult = new URL(subject); + if (!['https:', 'mailto:'].includes(subjectParseResult.protocol)) { + throw new Error('Vapid subject is not an https: or mailto: URL. ' + subject); } + if (subjectParseResult.hostname === 'localhost') { + console.warn('Vapid subject points to a localhost web URI, which is unsupported by ' + + 'Apple\'s push notification server and will result in a BadJwtToken error when ' + + 'sending notifications.'); + } + } catch (err) { + throw new Error('Vapid subject is not a valid URL. ' + subject); } } @@ -189,8 +193,9 @@ function getVapidHeaders(audience, subject, publicKey, privateKey, contentEncodi + 'origin of a push service. ' + audience); } - const audienceParseResult = url.parse(audience); - if (!audienceParseResult.hostname) { + try { + new URL(audience); // eslint-disable-line no-new + } catch (err) { throw new Error('VAPID audience is not a url. ' + audience); } diff --git a/test/test-vapid-helper.js b/test/test-vapid-helper.js index 730b3822..071a44fc 100644 --- a/test/test-vapid-helper.js +++ b/test/test-vapid-helper.js @@ -9,8 +9,12 @@ const webPush = require('../src/index'); const vapidHelper = require('../src/vapid-helper'); const VALID_AUDIENCE = 'https://example.com'; -const VALID_SUBJECT_MAILTO = 'mailto: example@example.com'; -const VALID_SUBJECT_URL = 'https://exampe.com/contact'; +const VALID_SUBJECT_MAILTO = 'mailto:example@example.com'; +const VALID_SUBJECT_LOCALHOST_MAILTO = 'mailto:user@localhost'; +const VALID_SUBJECT_URL = 'https://example.com/contact'; +const WARN_SUBJECT_LOCALHOST_URL = 'https://localhost'; +const INVALID_SUBJECT_URL_1 = 'http://example.gov'; +const INVALID_SUBJECT_URL_2 = 'ftp://example.net'; const VALID_PUBLIC_KEY = urlBase64.encode(Buffer.alloc(65)); const VALID_UNSAFE_BASE64_PUBLIC_KEY = Buffer.alloc(65).toString('base64'); const VALID_PRIVATE_KEY = urlBase64.encode(Buffer.alloc(32)); @@ -101,6 +105,14 @@ suite('Test Vapid Helpers', function() { function() { vapidHelper.getVapidHeaders('Not a URL', VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY); }, + function() { + // http URL protocol + vapidHelper.getVapidHeaders(VALID_AUDIENCE, INVALID_SUBJECT_URL_1, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY); + }, + function() { + // ftp URL protocol + vapidHelper.getVapidHeaders(VALID_AUDIENCE, INVALID_SUBJECT_URL_2, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY); + }, function() { vapidHelper.getVapidHeaders(VALID_AUDIENCE, 'Some Random String', VALID_PUBLIC_KEY, VALID_PRIVATE_KEY); }, @@ -168,9 +180,17 @@ suite('Test Vapid Helpers', function() { function(contentEncoding) { return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding); }, + function(contentEncoding) { + // localhost https: subject; should pass, since we don't throw an error for this, just warn to console + return vapidHelper.getVapidHeaders(VALID_AUDIENCE, WARN_SUBJECT_LOCALHOST_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding); + }, function(contentEncoding) { return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding); }, + function(contentEncoding) { + // localhost mailto: subject + return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_LOCALHOST_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding); + }, function(contentEncoding) { return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding, VALID_EXPIRATION); },