fix(electron): tighten navigation review feedback

Restrict packaged file navigation to the app renderer, guard redirects, and handle fire-and-forget header override IPC failures.
This commit is contained in:
4gray committed 2026-05-23 19:05:38 +03:00
1 parent c36d404a72
commit 003a8774a1
6 files changed
+99 -37

No files matched your search

+13 -2
View File
@@ -71,10 +71,21 @@ describe('Electron app security helpers', () => {
).toBe(false);
});
it('allows packaged file navigation but rejects external web navigation', () => {
it('allows only the packaged renderer file in packaged navigation', () => {
expect(
isTrustedRendererNavigationUrl('file:///tmp/index.html', false)
isTrustedRendererNavigationUrl(
'file:///tmp/iptvnator/index.html',
false,
'/tmp/iptvnator/index.html'
)
).toBe(true);
expect(
isTrustedRendererNavigationUrl(
'file:///tmp/other/index.html',
false,
'/tmp/iptvnator/index.html'
)
).toBe(false);
expect(
isTrustedRendererNavigationUrl('https://example.com', false)
).toBe(false);
+47 -17
View File
@@ -1,5 +1,6 @@
import { app, BrowserWindow, Menu, screen, shell } from 'electron';
import { join } from 'path';
import { join, resolve } from 'path';
import { fileURLToPath } from 'url';
import { rendererAppName, rendererAppPort } from './constants';
import {
isRendererConsoleTraceEnabled,
@@ -24,6 +25,18 @@ function parseUrl(url: string): URL | null {
}
}
function getPackagedRendererIndexPath(): string {
return resolve(__dirname, '..', rendererAppName, 'index.html');
}
function getFilePathFromUrl(url: URL): string | null {
try {
return fileURLToPath(url);
} catch {
return null;
}
}
export function isExternalBrowserUrl(url: string): boolean {
const parsedUrl = parseUrl(url);
return Boolean(
@@ -33,7 +46,8 @@ export function isExternalBrowserUrl(url: string): boolean {
export function isTrustedRendererNavigationUrl(
url: string,
isDevelopmentMode: boolean
isDevelopmentMode: boolean,
packagedRendererIndexPath = getPackagedRendererIndexPath()
): boolean {
const parsedUrl = parseUrl(url);
@@ -42,7 +56,13 @@ export function isTrustedRendererNavigationUrl(
}
if (parsedUrl.protocol === 'file:') {
return !isDevelopmentMode;
const filePath = getFilePathFromUrl(parsedUrl);
if (isDevelopmentMode || !filePath) {
return false;
}
return resolve(filePath) === resolve(packagedRendererIndexPath);
}
if (!isDevelopmentMode) {
@@ -202,6 +222,21 @@ export default class App {
}
}
private static handleRendererNavigation(
event: Electron.Event,
url: string
): void {
if (isTrustedRendererNavigationUrl(url, App.isDevelopmentMode())) {
return;
}
event.preventDefault();
if (isExternalBrowserUrl(url)) {
shell.openExternal(url);
}
}
private static initMainWindow() {
const workAreaSize = screen.getPrimaryDisplay().workAreaSize;
const width = Math.min(1280, workAreaSize.width || 1280);
@@ -246,17 +281,14 @@ export default class App {
return { action: 'deny' };
});
App.mainWindow.webContents.on('will-navigate', (event, url) => {
if (isTrustedRendererNavigationUrl(url, App.isDevelopmentMode())) {
return;
}
event.preventDefault();
if (isExternalBrowserUrl(url)) {
shell.openExternal(url);
}
});
App.mainWindow.webContents.on(
'will-navigate',
App.handleRendererNavigation
);
App.mainWindow.webContents.on(
'will-redirect',
App.handleRendererNavigation
);
// Emitted when the window is closed.
App.mainWindow.on('closed', () => {
@@ -318,9 +350,7 @@ export default class App {
App.mainWindow.webContents.openDevTools();
}
} else {
App.mainWindow.loadFile(
join(__dirname, '..', rendererAppName, 'index.html')
);
App.mainWindow.loadFile(getPackagedRendererIndexPath());
}
}
+7 -4
View File
@@ -20,15 +20,18 @@ owner in the Electron backend.
## Navigation And External URLs
The main window owns two navigation gates:
The main window owns three navigation gates:
- `setWindowOpenHandler` denies every new window. `http:` and `https:` targets
are opened in the operating system browser through `shell.openExternal`.
- `will-navigate` allows only the trusted renderer URL. Development mode allows
`http://localhost:4200`, `http://127.0.0.1:4200`, and `http://[::1]:4200`.
Packaged mode allows the app's file-backed renderer. External web
navigations are denied in the app window and opened in the operating system
browser.
- `will-redirect` applies the same allow/deny rules so server-side redirects
cannot move the app window to an untrusted origin.
Packaged mode allows only the app's resolved `index.html` renderer file, not
arbitrary `file:` URLs. External web navigations are denied in the app window
and opened in the operating system browser.
Do not add broad protocol allow-lists for renderer navigation. If a new
desktop-only flow needs to open a URL outside IPTVnator, route it through the
+12 -5
View File
@@ -160,11 +160,18 @@ export class PlaylistEffects {
this.epgService.getChannelPrograms(channelId);
}
window.electron?.setUserAgent(
channel.http?.['user-agent'],
channel.http?.referrer,
channel.url
);
void window.electron
?.setUserAgent(
channel.http?.['user-agent'],
channel.http?.referrer,
channel.url
)
.catch((error: unknown) => {
console.warn(
'[PlaylistEffects] Failed to configure Electron request headers:',
error
);
});
firstValueFrom(this.storage.get(STORE_KEY.Settings)).then(
(settings: any) => {
@@ -98,10 +98,14 @@ export class M3uWorkspaceRouteSession {
return;
}
window.electron?.setUserAgent(
playlist.userAgent,
playlist.referrer
);
void window.electron
?.setUserAgent(playlist.userAgent, playlist.referrer)
.catch((error: unknown) => {
console.warn(
'[M3uWorkspaceRouteSession] Failed to configure Electron request headers:',
error
);
});
this.store.dispatch(
ChannelActions.setChannels({
@@ -150,11 +150,18 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
const url = channel.url + (channel.epgParams ?? '');
const extension = getPlaybackMediaExtensionFromUrl(channel.url);
window.electron?.setUserAgent(
channel.http?.['user-agent'],
channel.http?.referrer,
channel.url
);
void window.electron
?.setUserAgent(
channel.http?.['user-agent'],
channel.http?.referrer,
channel.url
)
.catch((error: unknown) => {
console.warn(
'[HtmlVideoPlayer] Failed to configure Electron request headers:',
error
);
});
if ((extension === 'ts' || !extension) && mpegts.isSupported()) {
debugHtmlPlayer(