mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
* fix(electron): open file:// URLs with special characters (#8695) Local file:// URLs with non-ASCII names or spaces (e.g. Grüne, "Another One") failed to open on Windows: both open sinks handed the URL to shell.openExternal, which percent-encodes the path (ü → %C3%BC, space → %20) before ShellExecute, so the OS then searched for a literally-named folder and failed. Pure-ASCII paths worked because there was nothing to encode. Route local file: URLs through shell.openPath with a decoded filesystem path (fileURLToPath) instead. openPath takes a raw path, so Unicode names and spaces open correctly. This also applies the executable-extension guard to file: URLs, which previously only ran on the OPEN_PATH sink (GHSA-hr87-735w-hfq3). Shared openLocalPath()/isLocalFileUrl() cover both the OPEN_EXTERNAL IPC handler and the navigation-interception path in main-window. * test(electron): cover NTLM/exec vectors at the un-pre-gated openPath sink Follow-up to the #8695 review. Add regression tests locking in that openLocalPath alone (OPEN_PATH has no scheme pre-gate) blocks the path-based UNC file: URL (file:////host/share, GHSA-hr87-735w-hfq3) and an executable hidden behind a percent-encoded dot (evil%2Ebat). Document that the decode tests run on Linux and therefore don't exercise the Windows drive-letter/backslash conversion, and clarify why isLocalFileUrl is intentionally broader than the renderer/scheme-allowlist checks.
59 lines
2.3 KiB
TypeScript
59 lines
2.3 KiB
TypeScript
import { app, dialog, ipcMain, shell } from 'electron';
|
|
import { IPC } from '../shared-with-frontend/ipc-events.const';
|
|
import { isExternalUrlSchemeAllowed } from '../shared-with-frontend/is-external-url-allowed';
|
|
import { isLocalFileUrl, openLocalPath } from '../open-url';
|
|
import { getWin } from '../main-window';
|
|
|
|
export const initSystemIpc = (): void => {
|
|
ipcMain.on(IPC.OPEN_PATH, (ev, path: string) => {
|
|
// openLocalPath enforces the guards this sink needs: reject UNC / remote
|
|
// file:// paths (NTLM-hash leak) and never launch an executable/script.
|
|
// FILE-type task-attachment paths are synced and thus attacker-controllable.
|
|
// See GHSA-hr87-735w-hfq3.
|
|
openLocalPath(path);
|
|
});
|
|
ipcMain.on(IPC.OPEN_EXTERNAL, (ev, url: string) => {
|
|
// Defense in depth: never hand an unsafe scheme to the OS handler, even if
|
|
// the renderer-side guard is bypassed. See GHSA-hr87-735w-hfq3.
|
|
if (!isExternalUrlSchemeAllowed(url)) {
|
|
return;
|
|
}
|
|
// A local file: URL opens reliably via openPath, not openExternal:
|
|
// openExternal percent-encodes the path on Windows and then can't resolve
|
|
// non-ASCII names or spaces. openLocalPath decodes it and re-applies the
|
|
// openPath guards. See issue #8695.
|
|
if (isLocalFileUrl(url)) {
|
|
openLocalPath(url);
|
|
return;
|
|
}
|
|
shell.openExternal(url);
|
|
});
|
|
|
|
ipcMain.on(
|
|
IPC.SHOW_EMOJI_PANEL,
|
|
() => app.isEmojiPanelSupported() && app.showEmojiPanel(),
|
|
);
|
|
|
|
ipcMain.handle(IPC.SAVE_FILE_DIALOG, async (ev, { filename, data }) => {
|
|
const result = await dialog.showSaveDialog(getWin(), {
|
|
defaultPath: filename,
|
|
filters: [
|
|
{ name: 'JSON Files', extensions: ['json'] },
|
|
{ name: 'All Files', extensions: ['*'] },
|
|
],
|
|
});
|
|
|
|
if (!result.canceled && result.filePath) {
|
|
const fs = await import('fs');
|
|
await fs.promises.writeFile(result.filePath, data, 'utf-8');
|
|
return { success: true, path: result.filePath };
|
|
}
|
|
return { success: false };
|
|
});
|
|
|
|
ipcMain.handle(IPC.SHARE_NATIVE, async () => {
|
|
// Desktop platforms use the share dialog instead of native share
|
|
// This allows for more flexibility and better UX with social media options
|
|
return { success: false, error: 'Native share not available on desktop' };
|
|
});
|
|
};
|