From 1c50a2c047ebdde5f805264d0fa2379cfbbe9666 Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:53:49 -0500 Subject: [PATCH] Improve file handler check to add path separator Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com> --- app/protocol_filter.node.ts | 17 +- ts/test-node/app/protocol_filter_test.node.ts | 161 +++++++++++++++++- 2 files changed, 173 insertions(+), 5 deletions(-) diff --git a/app/protocol_filter.node.ts b/app/protocol_filter.node.ts index 3d436e6d26..17c68f0a4b 100644 --- a/app/protocol_filter.node.ts +++ b/app/protocol_filter.node.ts @@ -16,11 +16,17 @@ import { getUpdateCachePath, } from './attachments.node.ts'; import { createLogger } from '../ts/logging/log.std.ts'; +import { isPathInside } from '../ts/util/isPathInside.node.ts'; const log = createLogger('protocol_filter'); type CallbackType = (response: string | ProtocolResponse) => void; +export type FileHandlerType = ( + request: ProtocolRequest, + callback: CallbackType +) => void; + function _eliminateAllAfterCharacter( string: string, character: string @@ -50,7 +56,7 @@ export function _urlToPath( return withoutQuerystring; } -function _createFileHandler({ +export function _createFileHandler({ userDataPath, installPath, isWindows, @@ -58,7 +64,7 @@ function _createFileHandler({ userDataPath: string; installPath: string; isWindows: boolean; -}) { +}): FileHandlerType { const allowedRoots = [ userDataPath, installPath, @@ -71,6 +77,9 @@ function _createFileHandler({ getTempPath(userDataPath), getUpdateCachePath(userDataPath), ]; + const allowedRootsCased = allowedRoots.map(root => + isWindows ? root.toLowerCase() : root + ); return (request: ProtocolRequest, callback: CallbackType): void => { let targetPath; @@ -102,8 +111,8 @@ function _createFileHandler({ return; } - for (const root of allowedRoots) { - if (properCasing.startsWith(isWindows ? root.toLowerCase() : root)) { + for (const root of allowedRootsCased) { + if (isPathInside(properCasing, root)) { callback({ path: realPath }); return; } diff --git a/ts/test-node/app/protocol_filter_test.node.ts b/ts/test-node/app/protocol_filter_test.node.ts index 5fca862f53..8830e8166a 100644 --- a/ts/test-node/app/protocol_filter_test.node.ts +++ b/ts/test-node/app/protocol_filter_test.node.ts @@ -2,10 +2,24 @@ // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; +import type { ProtocolResponse } from 'electron'; +import * as sinon from 'sinon'; -import { _urlToPath } from '../../../app/protocol_filter.node.ts'; +import { + _createFileHandler, + _urlToPath, + type FileHandlerType, +} from '../../../app/protocol_filter.node.ts'; describe('Protocol Filter', () => { + beforeEach(function (this: Mocha.Context) { + this.sandbox = sinon.createSandbox(); + }); + + afterEach(function (this: Mocha.Context) { + this.sandbox.restore(); + }); + describe('_urlToPath', () => { it('returns proper file path for unix style file URI with querystring', () => { const path = @@ -79,4 +93,149 @@ describe('Protocol Filter', () => { assert.strictEqual(actual, expected); }); }); + + describe('_createFileHandler', () => { + function testFn({ + fileHandler, + url, + expectedResult, + }: { + fileHandler: FileHandlerType; + url: string; + expectedResult: string | Partial; + }) { + const testCallback = sinon.stub(); + + const request = { + headers: {}, + method: 'GET', + referrer: '', + url, + }; + fileHandler(request, testCallback); + sinon.assert.calledWithMatch(testCallback, expectedResult); + } + + describe('windows', () => { + before(function (this: Mocha.Context) { + if (process.platform !== 'win32') { + this.skip(); + } + }); + + function getFileHandler(): FileHandlerType { + return _createFileHandler({ + userDataPath: 'C:\\Users\\signaluser\\AppData\\Roaming\\Signal', + installPath: + 'C:\\Users\\signaluser\\AppData\\Local\\Programs\\signal-desktop', + isWindows: true, + }); + } + + it('allows files in userData', () => { + testFn({ + fileHandler: getFileHandler(), + url: 'file:///C:/Users/signaluser/AppData/Roaming/Signal/foo', + expectedResult: { + path: 'C:\\Users\\signaluser\\AppData\\Roaming\\Signal\\foo', + }, + }); + }); + + it('disallows files in other places', () => { + const fileHandler = getFileHandler(); + + testFn({ + fileHandler, + url: 'file:///C:/Windows/foo', + expectedResult: { error: -10 }, + }); + testFn({ + fileHandler, + url: 'file:///C:/Users/signaluser/AppData/Roaming/Signal Beta/foo', + expectedResult: { error: -10 }, + }); + }); + }); + + describe('macos', () => { + before(function (this: Mocha.Context) { + if (process.platform === 'win32') { + this.skip(); + } + }); + + function getFileHandler(): FileHandlerType { + return _createFileHandler({ + userDataPath: '/Users/signaluser/Library/Application Support/Signal', + installPath: '/Applications/Signal', + isWindows: false, + }); + } + + it('allows files in userData', () => { + testFn({ + fileHandler: getFileHandler(), + url: 'file:///Users/signaluser/Library/Application Support/Signal/foo', + expectedResult: { + path: '/Users/signaluser/Library/Application Support/Signal/foo', + }, + }); + }); + + it('disallows files in other places', () => { + const fileHandler = getFileHandler(); + + testFn({ + fileHandler, + url: 'file:///Applications/foo', + expectedResult: { error: -10 }, + }); + testFn({ + fileHandler, + url: 'file:///Users/signaluser/Library/Application Support/Signal Beta/foo', + expectedResult: { error: -10 }, + }); + }); + }); + + describe('linux', () => { + before(function (this: Mocha.Context) { + if (process.platform === 'win32') { + this.skip(); + } + }); + + function getFileHandler(): FileHandlerType { + return _createFileHandler({ + userDataPath: '/home/signaluser/.config/Signal', + installPath: '/usr/bin/signal-desktop', + isWindows: false, + }); + } + + it('allows files in userData', () => { + testFn({ + fileHandler: getFileHandler(), + url: 'file:///home/signaluser/.config/Signal/foo', + expectedResult: { path: '/home/signaluser/.config/Signal/foo' }, + }); + }); + + it('disallows files in other places', () => { + const fileHandler = getFileHandler(); + + testFn({ + fileHandler, + url: 'file:///usr/bin/foo', + expectedResult: { error: -10 }, + }); + testFn({ + fileHandler, + url: 'file:///home/signaluser/.config/Signal Beta/foo', + expectedResult: { error: -10 }, + }); + }); + }); + }); });