mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
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:
1 parent
c36d404a72
commit
003a8774a1
6 files changed
+99
-37
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user