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.
49 lines
2 KiB
TypeScript
49 lines
2 KiB
TypeScript
import { shell } from 'electron';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
hasExecutableFileExtension,
|
|
isPathSafeToOpen,
|
|
} from './shared-with-frontend/is-external-url-allowed';
|
|
|
|
/**
|
|
* True for anything shaped like a `file:` URL. Intentionally broad: at the
|
|
* OPEN_PATH sink there is no `isExternalUrlSchemeAllowed` pre-gate, so this must
|
|
* catch every `file:`-shape value and hand it to `openLocalPath`, which
|
|
* re-validates the decoded path. (At the OPEN_EXTERNAL / navigation sinks,
|
|
* `isExternalUrlSchemeAllowed` has already narrowed the input to a canonical
|
|
* `file:///<path>`.) Note this is deliberately looser than that check and than
|
|
* the renderer's `startsWith('file://')` guard — the safety comes from the
|
|
* post-decode guards in `openLocalPath`, not from this test.
|
|
*/
|
|
export const isLocalFileUrl = (value: string): boolean => /^\s*file:/i.test(value);
|
|
|
|
/**
|
|
* Open a local filesystem path — or a local `file:` URL — with the OS default
|
|
* handler.
|
|
*
|
|
* A `file:` URL is decoded to a real filesystem path first: on Windows,
|
|
* `shell.openExternal` hands ShellExecute a Chromium-percent-encoded URL
|
|
* (`ü` → `%C3%BC`, space → `%20`), which then searches for a literally-named
|
|
* folder and fails to open it. `fileURLToPath` decodes those escapes and
|
|
* converts `/C:/…` → `C:\…`, so folders/files with non-ASCII names or spaces
|
|
* open correctly. See issue #8695.
|
|
*
|
|
* Enforces the two guards required at every `openPath` sink: reject UNC /
|
|
* remote paths (they make the OS reach a remote SMB host and leak the user's
|
|
* NTLM hash) and never launch an executable/script. See GHSA-hr87-735w-hfq3.
|
|
*/
|
|
export const openLocalPath = (pathOrFileUrl: string): void => {
|
|
let fsPath = pathOrFileUrl;
|
|
if (isLocalFileUrl(pathOrFileUrl)) {
|
|
try {
|
|
fsPath = fileURLToPath(pathOrFileUrl.trim());
|
|
} catch {
|
|
// Malformed file: URL (e.g. a remote authority fileURLToPath rejects).
|
|
return;
|
|
}
|
|
}
|
|
if (!isPathSafeToOpen(fsPath) || hasExecutableFileExtension(fsPath)) {
|
|
return;
|
|
}
|
|
shell.openPath(fsPath);
|
|
};
|