From 69826610a2a27fa11cfcb16c9d51146d5510a833 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 14 Jun 2023 15:51:35 +0100 Subject: [PATCH] Desktop: Security: Prevent calling arbitrary commands via x-callback-url --- packages/app-desktop/gui/MainScreen/MainScreen.tsx | 3 ++- packages/lib/callbackUrlUtils.test.ts | 3 ++- packages/lib/callbackUrlUtils.ts | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/app-desktop/gui/MainScreen/MainScreen.tsx b/packages/app-desktop/gui/MainScreen/MainScreen.tsx index b5fcf5b07..55af1b9b7 100644 --- a/packages/app-desktop/gui/MainScreen/MainScreen.tsx +++ b/packages/app-desktop/gui/MainScreen/MainScreen.tsx @@ -34,7 +34,7 @@ import ShareFolderDialog from '../ShareFolderDialog/ShareFolderDialog'; import { ShareInvitation } from '@joplin/lib/services/share/reducer'; import removeKeylessItems from '../ResizableLayout/utils/removeKeylessItems'; import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils'; -import { parseCallbackUrl } from '@joplin/lib/callbackUrlUtils'; +import { isCallbackUrl, parseCallbackUrl } from '@joplin/lib/callbackUrlUtils'; import ElectronAppWrapper from '../../ElectronAppWrapper'; import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils'; import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types'; @@ -173,6 +173,7 @@ class MainScreenComponent extends React.Component { } private openCallbackUrl(url: string) { + if (!isCallbackUrl(url)) throw new Error(`Invalid callback URL: ${url}`); const { command, params } = parseCallbackUrl(url); void CommandService.instance().execute(command.toString(), params.id); } diff --git a/packages/lib/callbackUrlUtils.test.ts b/packages/lib/callbackUrlUtils.test.ts index df175ff5a..c04a8ecc3 100644 --- a/packages/lib/callbackUrlUtils.test.ts +++ b/packages/lib/callbackUrlUtils.test.ts @@ -3,13 +3,14 @@ import * as callbackUrlUtils from './callbackUrlUtils'; describe('callbackUrlUtils', () => { it('should identify valid callback urls', () => { - const url = 'joplin://x-callback-url/123?a=b'; + const url = 'joplin://x-callback-url/openFolder?a=b'; expect(callbackUrlUtils.isCallbackUrl(url)).toBe(true); }); it('should identify invalid callback urls', () => { expect(callbackUrlUtils.isCallbackUrl('not-joplin://x-callback-url/123?a=b')).toBe(false); expect(callbackUrlUtils.isCallbackUrl('joplin://xcallbackurl/123?a=b')).toBe(false); + expect(callbackUrlUtils.isCallbackUrl('joplin://x-callback-url/invalidCommand?a=b')).toBe(false); }); it('should build valid note callback urls', () => { diff --git a/packages/lib/callbackUrlUtils.ts b/packages/lib/callbackUrlUtils.ts index 2018ead98..f5eb3f965 100644 --- a/packages/lib/callbackUrlUtils.ts +++ b/packages/lib/callbackUrlUtils.ts @@ -1,7 +1,9 @@ const URL = require('url-parse'); export function isCallbackUrl(s: string) { - return s.startsWith('joplin://x-callback-url/'); + return s.startsWith('joplin://x-callback-url/openNote?') || + s.startsWith('joplin://x-callback-url/openFolder?') || + s.startsWith('joplin://x-callback-url/openTag?'); } export function getNoteCallbackUrl(noteId: string) {