Skip to content

Commit 2eb5ea2

Browse files
authored
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
1 parent 0e6cb56 commit 2eb5ea2

File tree

2 files changed

+38
-13
lines changed

2 files changed

+38
-13
lines changed

src/vapid-helper.js

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const crypto = require('crypto');
44
const urlBase64 = require('urlsafe-base64');
55
const asn1 = require('asn1.js');
66
const jws = require('jws');
7-
const url = require('url');
7+
const { URL } = require('url');
88

99
const WebPushConstants = require('./web-push-constants.js');
1010

@@ -71,18 +71,22 @@ function validateSubject(subject) {
7171
}
7272

7373
if (typeof subject !== 'string' || subject.length === 0) {
74-
throw new Error('The subject value must be a string containing a URL or '
74+
throw new Error('The subject value must be a string containing an https: URL or '
7575
+ 'mailto: address. ' + subject);
7676
}
7777

78-
if (subject.indexOf('mailto:') !== 0) {
79-
const subjectParseResult = url.parse(subject);
80-
if (!subjectParseResult.hostname) {
81-
throw new Error('Vapid subject is not a url or mailto url. ' + subject);
82-
} else if (subjectParseResult.hostname === 'localhost' && subjectParseResult.protocol === 'https:') {
83-
console.warn('VAPID subject points to a localhost web URI, which is unsupported by Apple\'s push notification '
84-
+ 'server and will result in a BadJwtToken error when sending notifications.');
78+
try {
79+
const subjectParseResult = new URL(subject);
80+
if (!['https:', 'mailto:'].includes(subjectParseResult.protocol)) {
81+
throw new Error('Vapid subject is not an https: or mailto: URL. ' + subject);
8582
}
83+
if (subjectParseResult.hostname === 'localhost') {
84+
console.warn('Vapid subject points to a localhost web URI, which is unsupported by '
85+
+ 'Apple\'s push notification server and will result in a BadJwtToken error when '
86+
+ 'sending notifications.');
87+
}
88+
} catch (err) {
89+
throw new Error('Vapid subject is not a valid URL. ' + subject);
8690
}
8791
}
8892

@@ -189,8 +193,9 @@ function getVapidHeaders(audience, subject, publicKey, privateKey, contentEncodi
189193
+ 'origin of a push service. ' + audience);
190194
}
191195

192-
const audienceParseResult = url.parse(audience);
193-
if (!audienceParseResult.hostname) {
196+
try {
197+
new URL(audience); // eslint-disable-line no-new
198+
} catch (err) {
194199
throw new Error('VAPID audience is not a url. ' + audience);
195200
}
196201

test/test-vapid-helper.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ const webPush = require('../src/index');
99
const vapidHelper = require('../src/vapid-helper');
1010

1111
const VALID_AUDIENCE = 'https://example.com';
12-
const VALID_SUBJECT_MAILTO = 'mailto: [email protected]';
13-
const VALID_SUBJECT_URL = 'https://exampe.com/contact';
12+
const VALID_SUBJECT_MAILTO = 'mailto:[email protected]';
13+
const VALID_SUBJECT_LOCALHOST_MAILTO = 'mailto:user@localhost';
14+
const VALID_SUBJECT_URL = 'https://example.com/contact';
15+
const WARN_SUBJECT_LOCALHOST_URL = 'https://localhost';
16+
const INVALID_SUBJECT_URL_1 = 'http://example.gov';
17+
const INVALID_SUBJECT_URL_2 = 'ftp://example.net';
1418
const VALID_PUBLIC_KEY = urlBase64.encode(Buffer.alloc(65));
1519
const VALID_UNSAFE_BASE64_PUBLIC_KEY = Buffer.alloc(65).toString('base64');
1620
const VALID_PRIVATE_KEY = urlBase64.encode(Buffer.alloc(32));
@@ -101,6 +105,14 @@ suite('Test Vapid Helpers', function() {
101105
function() {
102106
vapidHelper.getVapidHeaders('Not a URL', VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY);
103107
},
108+
function() {
109+
// http URL protocol
110+
vapidHelper.getVapidHeaders(VALID_AUDIENCE, INVALID_SUBJECT_URL_1, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY);
111+
},
112+
function() {
113+
// ftp URL protocol
114+
vapidHelper.getVapidHeaders(VALID_AUDIENCE, INVALID_SUBJECT_URL_2, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY);
115+
},
104116
function() {
105117
vapidHelper.getVapidHeaders(VALID_AUDIENCE, 'Some Random String', VALID_PUBLIC_KEY, VALID_PRIVATE_KEY);
106118
},
@@ -168,9 +180,17 @@ suite('Test Vapid Helpers', function() {
168180
function(contentEncoding) {
169181
return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding);
170182
},
183+
function(contentEncoding) {
184+
// localhost https: subject; should pass, since we don't throw an error for this, just warn to console
185+
return vapidHelper.getVapidHeaders(VALID_AUDIENCE, WARN_SUBJECT_LOCALHOST_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding);
186+
},
171187
function(contentEncoding) {
172188
return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding);
173189
},
190+
function(contentEncoding) {
191+
// localhost mailto: subject
192+
return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_LOCALHOST_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding);
193+
},
174194
function(contentEncoding) {
175195
return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding, VALID_EXPIRATION);
176196
},

0 commit comments

Comments
 (0)