Skip to content

Commit dc98925

Browse files
authored
Stop redirecting to non-origin sites (#3710)
* Stop directing to non-origin sites * Create tall-mugs-shop.md
1 parent caed9c0 commit dc98925

File tree

3 files changed

+50
-14
lines changed

3 files changed

+50
-14
lines changed

.changeset/tall-mugs-shop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/messaging': patch
3+
---
4+
5+
stops redirecting user to non-origin urls.

packages/messaging/src/controllers/sw-controller.test.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ import { expect } from 'chai';
4848
import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies';
4949
import { getFakeTokenDetails } from '../testing/fakes/token-details';
5050

51+
const LOCAL_HOST = self.location.host;
52+
const TEST_LINK = 'https://' + LOCAL_HOST + '/test-link.org';
53+
const TEST_CLICK_ACTION = 'https://' + LOCAL_HOST + '/test-click-action.org';
54+
5155
// Add fake SW types.
5256
declare const self: Window & Writable<ServiceWorkerGlobalScope>;
5357

@@ -59,7 +63,7 @@ const DISPLAY_MESSAGE: MessagePayloadInternal = {
5963
body: 'body'
6064
},
6165
fcmOptions: {
62-
link: 'https://example.org'
66+
link: TEST_LINK
6367
},
6468
from: 'from',
6569
// eslint-disable-next-line camelcase
@@ -454,9 +458,29 @@ describe('SwController', () => {
454458
expect(matchAllSpy).not.to.have.been.called;
455459
});
456460

461+
it('does not redirect if link is not from origin', async () => {
462+
// Remove link.
463+
NOTIFICATION_CLICK_PAYLOAD.notification!.data![FCM_MSG].fcmOptions.link =
464+
'https://www.youtube.com';
465+
466+
const event = makeEvent('notificationclick', NOTIFICATION_CLICK_PAYLOAD);
467+
const stopImmediatePropagationSpy = spy(
468+
event,
469+
'stopImmediatePropagation'
470+
);
471+
const notificationCloseSpy = spy(event.notification, 'close');
472+
const matchAllSpy = spy(self.clients, 'matchAll');
473+
474+
await callEventListener(event);
475+
476+
expect(stopImmediatePropagationSpy).to.have.been.called;
477+
expect(notificationCloseSpy).to.have.been.called;
478+
expect(matchAllSpy).not.to.have.been.called;
479+
});
480+
457481
it('focuses on and sends the message to an open WindowClient', async () => {
458482
const client: Writable<WindowClient> = (await self.clients.openWindow(
459-
'https://example.org'
483+
TEST_LINK
460484
))!;
461485
const focusSpy = spy(client, 'focus');
462486
const matchAllSpy = spy(self.clients, 'matchAll');
@@ -485,15 +509,15 @@ describe('SwController', () => {
485509
await callEventListener(event);
486510

487511
expect(matchAllSpy).to.have.been.called;
488-
expect(openWindowSpy).to.have.been.calledWith('https://example.org');
512+
expect(openWindowSpy).to.have.been.calledWith(TEST_LINK);
489513
});
490514

491515
it('works with click_action', async () => {
492516
// Replace link with the deprecated click_action.
493517
delete NOTIFICATION_CLICK_PAYLOAD.notification!.data![FCM_MSG].fcmOptions;
494518
NOTIFICATION_CLICK_PAYLOAD.notification!.data![
495519
FCM_MSG
496-
].notification.click_action = 'https://example.org'; // eslint-disable-line camelcase
520+
].notification.click_action = TEST_CLICK_ACTION; // eslint-disable-line camelcase
497521

498522
const matchAllSpy = spy(self.clients, 'matchAll');
499523
const openWindowSpy = spy(self.clients, 'openWindow');
@@ -503,7 +527,7 @@ describe('SwController', () => {
503527
await callEventListener(event);
504528

505529
expect(matchAllSpy).to.have.been.called;
506-
expect(openWindowSpy).to.have.been.calledWith('https://example.org');
530+
expect(openWindowSpy).to.have.been.calledWith(TEST_CLICK_ACTION);
507531
});
508532

509533
it('redirects to origin if message was sent from the FN Console', async () => {

packages/messaging/src/controllers/sw-controller.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -246,15 +246,25 @@ export class SwController implements FirebaseMessaging, FirebaseService {
246246
event.stopImmediatePropagation();
247247
event.notification.close();
248248

249+
// Note clicking on a notification with no link set will focus the Chrome's current tab.
249250
const link = getLink(internalPayload);
250251
if (!link) {
251252
return;
252253
}
253254

254-
let client = await getWindowClient(link);
255+
// FM should only open/focus links from app's origin.
256+
const url = new URL(link, self.location.href);
257+
const originUrl = new URL(self.location.origin);
258+
259+
if (url.host !== originUrl.host) {
260+
return;
261+
}
262+
263+
let client = await getWindowClient(url);
264+
255265
if (!client) {
256-
// Unable to find window client so need to open one. This also focuses the opened client.
257266
client = await self.clients.openWindow(link);
267+
258268
// Wait three seconds for the client to initialize and set up the message handler so that it
259269
// can receive the message.
260270
await sleep(3000);
@@ -309,16 +319,13 @@ function getMessagePayloadInternal({
309319
* @param url The URL to look for when focusing a client.
310320
* @return Returns an existing window client or a newly opened WindowClient.
311321
*/
312-
async function getWindowClient(url: string): Promise<WindowClient | null> {
313-
// Use URL to normalize the URL when comparing to windowClients. This at least handles whether to
314-
// include trailing slashes or not
315-
const parsedURL = new URL(url, self.location.href);
316-
322+
async function getWindowClient(url: URL): Promise<WindowClient | null> {
317323
const clientList = await getClientList();
318324

319325
for (const client of clientList) {
320-
const parsedClientUrl = new URL(client.url, self.location.href);
321-
if (parsedClientUrl.host === parsedURL.host) {
326+
const clientUrl = new URL(client.url, self.location.href);
327+
328+
if (url.host === clientUrl.host) {
322329
return client;
323330
}
324331
}

0 commit comments

Comments
 (0)