mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Desktop: Security: Disallow UNC file links (#9979)
This commit is contained in:
parent
4c5e708977
commit
836e23c082
@ -586,7 +586,11 @@ class Application extends BaseApplication {
|
|||||||
ExternalEditWatcher.instance().setLogger(reg.logger());
|
ExternalEditWatcher.instance().setLogger(reg.logger());
|
||||||
ExternalEditWatcher.instance().initialize(bridge, this.store().dispatch);
|
ExternalEditWatcher.instance().initialize(bridge, this.store().dispatch);
|
||||||
|
|
||||||
ResourceEditWatcher.instance().initialize(reg.logger(), (action: any) => { this.store().dispatch(action); }, (path: string) => bridge().openItem(path));
|
ResourceEditWatcher.instance().initialize(
|
||||||
|
reg.logger(),
|
||||||
|
(action: any) => { this.store().dispatch(action); },
|
||||||
|
(path: string) => bridge().openItem(path),
|
||||||
|
);
|
||||||
|
|
||||||
// Forwards the local event to the global event manager, so that it can
|
// Forwards the local event to the global event manager, so that it can
|
||||||
// be picked up by the plugin manager.
|
// be picked up by the plugin manager.
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import ElectronAppWrapper from './ElectronAppWrapper';
|
import ElectronAppWrapper from './ElectronAppWrapper';
|
||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
import { _, setLocale } from '@joplin/lib/locale';
|
import { _, setLocale } from '@joplin/lib/locale';
|
||||||
import { BrowserWindow, nativeTheme, nativeImage } from 'electron';
|
import { BrowserWindow, nativeTheme, nativeImage, shell } from 'electron';
|
||||||
|
import { dirname, isUncPath, toSystemSlashes } from '@joplin/lib/path-utils';
|
||||||
|
import { fileUriToPath } from '@joplin/utils/url';
|
||||||
|
import { urlDecode } from '@joplin/lib/string-utils';
|
||||||
import * as Sentry from '@sentry/electron/main';
|
import * as Sentry from '@sentry/electron/main';
|
||||||
import { writeFileSync } from 'fs';
|
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import { msleep } from '@joplin/utils/time';
|
import { msleep } from '@joplin/utils/time';
|
||||||
const { dirname, toSystemSlashes } = require('@joplin/lib/path-utils');
|
import { pathExists, writeFileSync } from 'fs-extra';
|
||||||
|
import { normalize } from 'path';
|
||||||
|
|
||||||
interface LastSelectedPath {
|
interface LastSelectedPath {
|
||||||
file: string;
|
file: string;
|
||||||
@ -81,6 +84,11 @@ export class Bridge {
|
|||||||
return this.rootProfileDir_;
|
return this.rootProfileDir_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private logWarning(...message: string[]) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('bridge:', ...message);
|
||||||
|
}
|
||||||
|
|
||||||
public electronApp() {
|
public electronApp() {
|
||||||
return this.electronWrapper_;
|
return this.electronWrapper_;
|
||||||
}
|
}
|
||||||
@ -89,6 +97,14 @@ export class Bridge {
|
|||||||
return !this.electronApp().electronApp().isPackaged;
|
return !this.electronApp().electronApp().isPackaged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get autoUploadCrashDumps() {
|
||||||
|
return this.autoUploadCrashDumps_;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set autoUploadCrashDumps(v: boolean) {
|
||||||
|
this.autoUploadCrashDumps_ = v;
|
||||||
|
}
|
||||||
|
|
||||||
public async captureException(error: any) {
|
public async captureException(error: any) {
|
||||||
Sentry.captureException(error);
|
Sentry.captureException(error);
|
||||||
// We wait to give the "beforeSend" event handler time to process the crash dump and write
|
// We wait to give the "beforeSend" event handler time to process the crash dump and write
|
||||||
@ -300,12 +316,32 @@ export class Bridge {
|
|||||||
return require('electron').MenuItem;
|
return require('electron').MenuItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
public openExternal(url: string) {
|
public async openExternal(url: string) {
|
||||||
return require('electron').shell.openExternal(url);
|
const protocol = new URL(url).protocol;
|
||||||
|
|
||||||
|
if (protocol === 'file:') {
|
||||||
|
await this.openItem(url);
|
||||||
|
} else {
|
||||||
|
return shell.openExternal(url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openItem(fullPath: string) {
|
public async openItem(fullPath: string) {
|
||||||
return require('electron').shell.openPath(toSystemSlashes(fullPath));
|
if (fullPath.startsWith('file:/')) {
|
||||||
|
fullPath = fileUriToPath(urlDecode(fullPath), shim.platformName());
|
||||||
|
}
|
||||||
|
fullPath = normalize(fullPath);
|
||||||
|
// On Windows, \\example.com\... links can map to network drives. Opening files on these
|
||||||
|
// drives can lead to arbitrary remote code execution.
|
||||||
|
const isUntrustedUncPath = isUncPath(fullPath);
|
||||||
|
if (isUntrustedUncPath) {
|
||||||
|
this.logWarning(`Not opening external file link: ${fullPath} -- it starts with two \\s, so could be to a network drive.`);
|
||||||
|
return 'Refusing to open file on a network drive.';
|
||||||
|
} else if (await pathExists(fullPath)) {
|
||||||
|
return shell.openPath(fullPath);
|
||||||
|
} else {
|
||||||
|
return 'Path does not exist.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public screen() {
|
public screen() {
|
||||||
|
@ -22,7 +22,7 @@ export const runtime = (): CommandRuntime => {
|
|||||||
const { itemId, hash } = parsedUrl;
|
const { itemId, hash } = parsedUrl;
|
||||||
await openItemById(itemId, context.dispatch, hash);
|
await openItemById(itemId, context.dispatch, hash);
|
||||||
} else {
|
} else {
|
||||||
void require('electron').shell.openExternal(link);
|
void bridge().openExternal(link);
|
||||||
}
|
}
|
||||||
} else if (urlProtocol(link)) {
|
} else if (urlProtocol(link)) {
|
||||||
if (link.indexOf('file://') === 0) {
|
if (link.indexOf('file://') === 0) {
|
||||||
@ -33,9 +33,9 @@ export const runtime = (): CommandRuntime => {
|
|||||||
// but doesn't on macOS, so we need to convert it to a path
|
// but doesn't on macOS, so we need to convert it to a path
|
||||||
// before passing it to openPath.
|
// before passing it to openPath.
|
||||||
const decodedPath = fileUriToPath(urlDecode(link), shim.platformName());
|
const decodedPath = fileUriToPath(urlDecode(link), shim.platformName());
|
||||||
void require('electron').shell.openPath(decodedPath);
|
void bridge().openItem(decodedPath);
|
||||||
} else {
|
} else {
|
||||||
void require('electron').shell.openExternal(link);
|
void bridge().openExternal(link);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bridge().showErrorMessageBox(_('Unsupported link or message: %s', link));
|
bridge().showErrorMessageBox(_('Unsupported link or message: %s', link));
|
||||||
|
@ -15,7 +15,6 @@ import { NoteEntity, RevisionEntity } from '@joplin/lib/services/database/types'
|
|||||||
import { AppState } from '../app.reducer';
|
import { AppState } from '../app.reducer';
|
||||||
const urlUtils = require('@joplin/lib/urlUtils');
|
const urlUtils = require('@joplin/lib/urlUtils');
|
||||||
const ReactTooltip = require('react-tooltip');
|
const ReactTooltip = require('react-tooltip');
|
||||||
const { urlDecode } = require('@joplin/lib/string-utils');
|
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
import shared from '@joplin/lib/components/shared/note-screen-shared';
|
import shared from '@joplin/lib/components/shared/note-screen-shared';
|
||||||
|
|
||||||
@ -169,11 +168,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
|
|||||||
if (msg.indexOf('joplin://') === 0) {
|
if (msg.indexOf('joplin://') === 0) {
|
||||||
throw new Error(_('Unsupported link or message: %s', msg));
|
throw new Error(_('Unsupported link or message: %s', msg));
|
||||||
} else if (urlUtils.urlProtocol(msg)) {
|
} else if (urlUtils.urlProtocol(msg)) {
|
||||||
if (msg.indexOf('file://') === 0) {
|
await bridge().openExternal(msg);
|
||||||
void require('electron').shell.openExternal(urlDecode(msg));
|
|
||||||
} else {
|
|
||||||
void require('electron').shell.openExternal(msg);
|
|
||||||
}
|
|
||||||
} else if (msg.indexOf('#') === 0) {
|
} else if (msg.indexOf('#') === 0) {
|
||||||
// This is an internal anchor, which is handled by the WebView so skip this case
|
// This is an internal anchor, which is handled by the WebView so skip this case
|
||||||
} else {
|
} else {
|
||||||
|
@ -4,7 +4,7 @@ import { _ } from '@joplin/lib/locale';
|
|||||||
|
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const { themeStyle } = require('@joplin/lib/theme');
|
const { themeStyle } = require('@joplin/lib/theme');
|
||||||
const bridge = require('@electron/remote').require('./bridge').default;
|
import bridge from '../services/bridge';
|
||||||
const prettyBytes = require('pretty-bytes');
|
const prettyBytes = require('pretty-bytes');
|
||||||
import Resource from '@joplin/lib/models/Resource';
|
import Resource from '@joplin/lib/models/Resource';
|
||||||
|
|
||||||
@ -187,7 +187,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
|||||||
|
|
||||||
public openResource(resource: InnerResource) {
|
public openResource(resource: InnerResource) {
|
||||||
const resourcePath = Resource.fullPath(resource);
|
const resourcePath = Resource.fullPath(resource);
|
||||||
const ok = bridge().openExternal(`file://${resourcePath}`);
|
const ok = bridge().openItem(resourcePath);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
bridge().showErrorMessageBox(`This file could not be opened: ${resourcePath}`);
|
bridge().showErrorMessageBox(`This file could not be opened: ${resourcePath}`);
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ const textEditorCommand = () => {
|
|||||||
export const openFileWithExternalEditor = async (filePath: string, bridge: any) => {
|
export const openFileWithExternalEditor = async (filePath: string, bridge: any) => {
|
||||||
const cmd = textEditorCommand();
|
const cmd = textEditorCommand();
|
||||||
if (!cmd) {
|
if (!cmd) {
|
||||||
bridge.openExternal(`file://${filePath}`);
|
bridge.openItem(filePath);
|
||||||
} else {
|
} else {
|
||||||
cmd.args.push(filePath);
|
cmd.args.push(filePath);
|
||||||
await spawnCommand(cmd.path, cmd.args, { detached: true });
|
await spawnCommand(cmd.path, cmd.args, { detached: true });
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { extractExecutablePath, quotePath, toFileProtocolPath, unquotePath } from './path';
|
import { extractExecutablePath, isUncPath, quotePath, toFileProtocolPath, unquotePath } from './path';
|
||||||
|
|
||||||
describe('path', () => {
|
describe('path', () => {
|
||||||
it('should quote and unquote paths', (async () => {
|
it('should quote and unquote paths', (async () => {
|
||||||
@ -55,4 +55,22 @@ describe('path', () => {
|
|||||||
expect(toFileProtocolPath(t[0], 'linux')).toBe(t[1]);
|
expect(toFileProtocolPath(t[0], 'linux')).toBe(t[1]);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
['./a.txt', 'win32', false],
|
||||||
|
['./b.txt', 'win32', false],
|
||||||
|
['/home/foo/bar/baz', 'win32', false],
|
||||||
|
['./a.txt', 'posix', false],
|
||||||
|
['./b.txt', 'posix', false],
|
||||||
|
['/home/foo/bar/baz', 'posix', false],
|
||||||
|
|
||||||
|
['//LOCALHOST/', 'win32', true],
|
||||||
|
[' //LOCALHOST/', 'win32', true],
|
||||||
|
[' //example.com/a/b/c', 'win32', true],
|
||||||
|
['//LOCALHOST/', 'posix', false],
|
||||||
|
[' //example.com/a/b/c', 'posix', false],
|
||||||
|
['\\\\LOCALHOST/', 'win32', true],
|
||||||
|
])('should correctly detect UNC paths', (path, os, expected) => {
|
||||||
|
expect(isUncPath(path, os)).toBe(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -94,6 +94,13 @@ export function trimSlashes(path: string): string {
|
|||||||
return ltrimSlashes(rtrimSlashes(path));
|
return ltrimSlashes(rtrimSlashes(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UNC paths can point to network drives and thus can be dangerous to open
|
||||||
|
// on some Windows devices.
|
||||||
|
// See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/62e862f4-2a51-452e-8eeb-dc4ff5ee33cc
|
||||||
|
export const isUncPath = (path: string, os: string|null = null) => {
|
||||||
|
return toSystemSlashes(path.trim(), os).startsWith('\\\\');
|
||||||
|
};
|
||||||
|
|
||||||
export function quotePath(path: string) {
|
export function quotePath(path: string) {
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
if (path.indexOf('"') < 0 && path.indexOf(' ') < 0) return path;
|
if (path.indexOf('"') < 0 && path.indexOf(' ') < 0) return path;
|
||||||
|
Loading…
Reference in New Issue
Block a user