mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
* fix(electron): lock in-window navigation to the app's loaded origin The previous will-navigate handler accepted any URL whose hostname was 'localhost' or '127.0.0.1', which left the privileged main window reachable by any local web server (e.g. http://127.0.0.1:1337/) — the preload bridge (window.ea) would have been exposed to whatever page that server returned. In production the legitimate origin is file:// only, so there is no excuse for ever navigating to http://localhost in-window. Compare against the URL the app actually loaded (captured at loadURL time) instead of a hostname allowlist. http/https requires exact host+port match; file:// requires the same html pathname; data:/blob:/ javascript: are rejected outright. The same guard is also applied to will-redirect so a same-origin start cannot be redirected onto an attacker page mid-navigation, and a defensive did-create-window handler destroys any unexpected child window (the deny-all setWindowOpenHandler should make that path unreachable). Also tightens TO_FILE_URL with the same userData deny used by the other file-sync IPCs, so a plugin/XSS cannot launder a userData path into a file:// URL that later flows through READ_LOCAL_IMAGE_AS_DATA_URL. * fix(electron): reject UNC/remote-host file:// in navigation guard `new URL('file://192.168.1.100/Applications/SP.app/Contents/Resources/index.html').pathname` equals the local app's pathname, so the previous pathname-only check considered an attacker-controlled UNC host same-origin and let it load in the privileged main window. The app's loaded file:// URL is always local (empty host), so require `target.host === ''` explicitly. * test(electron): add userinfo-@-trick navigation guard regression `http://localhost:4200@evil.com/` parses with host=evil.com and username=localhost — a naive substring or startsWith check would be fooled. The existing host-equality check already rejects it; lock that in with a test so a future refactor cannot regress.
46 lines
2 KiB
TypeScript
46 lines
2 KiB
TypeScript
/**
|
|
* Returns true if `targetUrl` is the same origin as the app's loaded URL.
|
|
*
|
|
* Security boundary: the main window has Node integration / preload bridge.
|
|
* A navigation to ANY other origin in-window would expose `window.ea` to
|
|
* untrusted content (e.g. http://127.0.0.1:<any> hosting a malicious page).
|
|
* `will-navigate` MUST reject anything this returns false for and hand it
|
|
* to the (scheme-guarded) external-open path instead.
|
|
*
|
|
* Comparison rules:
|
|
* - Protocols must match exactly.
|
|
* - http/https: exact host (incl. port) match. Subdomains differ → different
|
|
* origin. This is what blocks `localhost.evil.com` and `http://127.0.0.1:1`.
|
|
* - file: BOTH host AND pathname must match the app's loaded html file.
|
|
* `file://` URLs may carry a host (UNC paths / remote shares), and Chromium
|
|
* resolves the pathname relative to that host — so a target like
|
|
* `file://192.168.1.100/Applications/SP.app/Contents/Resources/index.html`
|
|
* has the same `.pathname` as the local app start URL. A pathname-only
|
|
* check would let an attacker-controlled UNC host load in the privileged
|
|
* window. The app's loaded file:// URL is always local (empty host), so we
|
|
* require `target.host === ''` explicitly rather than relying on the
|
|
* host-equality compare to coincidentally match.
|
|
* Hash-only changes do not fire `will-navigate`, so a same-document hash
|
|
* route never reaches this check.
|
|
* - Anything else (data:, blob:, javascript:, ftp:, …): rejected.
|
|
*/
|
|
export const isAppOriginUrl = (targetUrl: string, appUrl: string): boolean => {
|
|
let target: URL;
|
|
let expected: URL;
|
|
try {
|
|
target = new URL(targetUrl);
|
|
expected = new URL(appUrl);
|
|
} catch {
|
|
return false;
|
|
}
|
|
if (target.protocol !== expected.protocol) return false;
|
|
if (target.protocol === 'http:' || target.protocol === 'https:') {
|
|
return target.host === expected.host;
|
|
}
|
|
if (target.protocol === 'file:') {
|
|
return (
|
|
target.host === '' && expected.host === '' && target.pathname === expected.pathname
|
|
);
|
|
}
|
|
return false;
|
|
};
|