Skip to content

Stop redirecting to non-origin sites #3710

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tall-mugs-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/messaging': patch
---

stops redirecting user to non-origin urls.
34 changes: 29 additions & 5 deletions packages/messaging/src/controllers/sw-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ import { expect } from 'chai';
import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies';
import { getFakeTokenDetails } from '../testing/fakes/token-details';

const LOCAL_HOST = self.location.host;
const TEST_LINK = 'https://' + LOCAL_HOST + '/test-link.org';
const TEST_CLICK_ACTION = 'https://' + LOCAL_HOST + '/test-click-action.org';

// Add fake SW types.
declare const self: Window & Writable<ServiceWorkerGlobalScope>;

Expand All @@ -59,7 +63,7 @@ const DISPLAY_MESSAGE: MessagePayloadInternal = {
body: 'body'
},
fcmOptions: {
link: 'https://example.org'
link: TEST_LINK
},
from: 'from',
// eslint-disable-next-line camelcase
Expand Down Expand Up @@ -454,9 +458,29 @@ describe('SwController', () => {
expect(matchAllSpy).not.to.have.been.called;
});

it('does not redirect if link is not from origin', async () => {
// Remove link.
NOTIFICATION_CLICK_PAYLOAD.notification!.data![FCM_MSG].fcmOptions.link =
'https://www.youtube.com';

const event = makeEvent('notificationclick', NOTIFICATION_CLICK_PAYLOAD);
const stopImmediatePropagationSpy = spy(
event,
'stopImmediatePropagation'
);
const notificationCloseSpy = spy(event.notification, 'close');
const matchAllSpy = spy(self.clients, 'matchAll');

await callEventListener(event);

expect(stopImmediatePropagationSpy).to.have.been.called;
expect(notificationCloseSpy).to.have.been.called;
expect(matchAllSpy).not.to.have.been.called;
});

it('focuses on and sends the message to an open WindowClient', async () => {
const client: Writable<WindowClient> = (await self.clients.openWindow(
'https://example.org'
TEST_LINK
))!;
const focusSpy = spy(client, 'focus');
const matchAllSpy = spy(self.clients, 'matchAll');
Expand Down Expand Up @@ -485,15 +509,15 @@ describe('SwController', () => {
await callEventListener(event);

expect(matchAllSpy).to.have.been.called;
expect(openWindowSpy).to.have.been.calledWith('https://example.org');
expect(openWindowSpy).to.have.been.calledWith(TEST_LINK);
});

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

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

expect(matchAllSpy).to.have.been.called;
expect(openWindowSpy).to.have.been.calledWith('https://example.org');
expect(openWindowSpy).to.have.been.calledWith(TEST_CLICK_ACTION);
});

it('redirects to origin if message was sent from the FN Console', async () => {
Expand Down
25 changes: 16 additions & 9 deletions packages/messaging/src/controllers/sw-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,15 +246,25 @@ export class SwController implements FirebaseMessaging, FirebaseService {
event.stopImmediatePropagation();
event.notification.close();

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

let client = await getWindowClient(link);
// FM should only open/focus links from app's origin.
const url = new URL(link, self.location.href);
const originUrl = new URL(self.location.origin);

if (url.host !== originUrl.host) {
return;
}

let client = await getWindowClient(url);

if (!client) {
// Unable to find window client so need to open one. This also focuses the opened client.
client = await self.clients.openWindow(link);

// Wait three seconds for the client to initialize and set up the message handler so that it
// can receive the message.
await sleep(3000);
Expand Down Expand Up @@ -309,16 +319,13 @@ function getMessagePayloadInternal({
* @param url The URL to look for when focusing a client.
* @return Returns an existing window client or a newly opened WindowClient.
*/
async function getWindowClient(url: string): Promise<WindowClient | null> {
// Use URL to normalize the URL when comparing to windowClients. This at least handles whether to
// include trailing slashes or not
const parsedURL = new URL(url, self.location.href);

async function getWindowClient(url: URL): Promise<WindowClient | null> {
const clientList = await getClientList();

for (const client of clientList) {
const parsedClientUrl = new URL(client.url, self.location.href);
if (parsedClientUrl.host === parsedURL.host) {
const clientUrl = new URL(client.url, self.location.href);

if (url.host === clientUrl.host) {
return client;
}
}
Expand Down