* fix(stalker): keep session headers on same-host redirects Since 0.22 requestWithValidatedRedirects stripped Cookie/Authorization whenever a redirect changed the *origin*, so a portal answering with an http->https upgrade or a port move lost the MAC cookie and Bearer token mid-session. Real Stalker/Ministra servers then reply with a plain-text "Authorization failed." body: categories fail to load, create_link never resolves, and no player receives a stream URL (#1158 regression window). Scope credential stripping to the host instead: same-host scheme/port redirects keep headers, basic auth, params, and request bodies; a redirect to a different host still drops all of them, preserving the original hardening intent (no credential leaks to third-party hosts). Also adds the Stalker API compatibility roadmap produced by the 2026-08-01 protocol audit (.plans/, force-added like earlier plans). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(electron): strip credentials on same-host https-to-http downgrades Review follow-up (Greptile P1 + Codex on #1322): the host-only check kept Authorization/Cookie/basic auth/params/body when an https request was redirected to http on the same host, replaying a TLS-obtained session in cleartext. Treat that downgrade like a host change: strip credentials and refuse to replay request bodies. Scheme upgrades and port moves on the same host keep headers — the actual #1158 scenarios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
12 KiB
Electron Security Contract
This document records the Electron runtime security contract for the desktop app.
BrowserWindow Defaults
The main window is created in apps/electron-backend/src/app/app.ts with an
explicit hardened webPreferences object:
contextIsolation: truenodeIntegration: falsesandbox: !frameCopyExperiment—trueby default; the opt-in Embedded MPV frame-copy experiment is the one path that disables the renderer sandbox (contextIsolation/nodeIntegrationstay hardened regardless)webSecurity: truepreload: apps/electron-backend/src/app/api/main.preload.ts
Renderer code must use the preload bridge exposed as window.electron.
Do not re-enable direct Node.js access from Angular code. New desktop-only APIs
should be added to the preload bridge and backed by an ipcMain.handle(...)
owner in the Electron backend.
Preload API Type Contract
The canonical renderer bridge type is
libs/shared/interfaces/src/lib/electron-api.interface.ts.
Keep these surfaces in sync when adding or changing a preload method:
ElectronBridgeApiin@iptvnator/shared/interfacesapps/electron-backend/src/app/api/main.preload.ts- the owning
ipcMain.handle(...)event module - renderer capability checks or runtime bridge services that consume the method
global.d.ts and apps/web/src/typings.d.ts should reference
ElectronBridgeApi instead of redeclaring window.electron method lists.
main.preload.ts is typed as ElectronBridgeApi, so missing or extra preload
methods fail typecheck instead of silently drifting from renderer typings.
Navigation And External URLs
The main window owns three navigation gates:
setWindowOpenHandlerdenies every new window.http:andhttps:targets are opened in the operating system browser throughshell.openExternal.will-navigateallows only the trusted renderer URL. Development mode allowshttp://localhost:4200,http://127.0.0.1:4200, andhttp://[::1]:4200.will-redirectapplies 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 default browser unless the app window is deliberately meant to host that URL.
Desktop Auto Updates
Desktop application updates are owned by the Electron main process. Renderer code talks only through the typed preload bridge:
window.electron.getAppUpdateStatus()window.electron.checkForAppUpdate()window.electron.downloadAppUpdate()window.electron.installAppUpdate()window.electron.getAppUpdateReleaseNotes(...)window.electron.onAppUpdateStatusChange(...)
The backend implementation lives in
apps/electron-backend/src/app/services/app-update.service.ts and registers IPC
handlers through apps/electron-backend/src/app/events/app-update.events.ts.
The service uses electron-updater with autoDownload = false, so renderer
actions explicitly ask to download and install. The main process starts one
packaged startup check after IPC registration; it never downloads during that
check. After a successful download, the renderer may show a restart/install
action backed by quitAndInstall().
Self-update is supported only for packaged macOS builds, Windows NSIS builds,
and Linux AppImage builds. Other Linux packages still point users to the latest
GitHub Release through manualDownloadUrl because Snap, Flatpak, DEB, RPM, and
Pacman installs have package-manager/store-specific update ownership. Packaged
non-AppImage Linux builds may still check GitHub Releases for a newer stable
version, but their update action remains a browser handoff to GitHub.
Release notes are also fetched by the main process from the public GitHub Releases API. The updater service caches release pages lazily and filters out draft/prerelease entries before returning Markdown to the renderer. The Angular dialog renders that Markdown with the local Markdown renderer and Angular HTML sanitization; do not bypass sanitizer for release body content.
Release CI must publish both installer artifacts and updater metadata generated
by electron-builder (latest.yml, latest-mac.yml, latest-linux*.yml, and
blockmap files when present). macOS must keep a signed/notarized app bundle,
produce both dmg and zip targets, and publish a single merged
latest-mac.yml containing both x64 and arm64 zip entries.
Content Security Policy
The Angular shell defines a baseline CSP in apps/web/src/index.html.
The policy keeps the application self-hosted for scripts, blocks object
embedding (object-src 'none') while allowing frames only from
https://www.youtube-nocookie.com (frame-src, used for TMDB trailers),
limits forms to the app origin, and allows IPTV playback sources through
media-src and connect-src for http:, https:, blob:, and data:. The
policy keeps script-src self-hosted and currently keeps unsafe-inline for
existing inline styles.
Angular production builds must not rely on inline event handlers for stylesheet
activation. Keep web:build:production and web:build:pwa configured without
critical CSS stylesheet deferral (optimization.styles.inlineCritical: false)
unless the CSP is intentionally changed and runtime-validated in both Electron
and the self-hosted PWA.
Before tightening either value, validate both Electron development startup and the PWA/electron build configurations. Playback-heavy changes should also check that HLS, MPEG-TS, thumbnails, and local file playback are still allowed by the policy.
Scoped Request Header Overrides
Inline playback can request temporary User-Agent, Referer, and Origin
header overrides through window.electron.setUserAgent(userAgent, referer, scopeUrl).
The Electron backend handles that IPC in apps/electron-backend/src/app/events/shared.events.ts
and delegates to apps/electron-backend/src/app/services/request-header-overrides.service.ts.
The service registers one session.defaultSession.webRequest.onBeforeSendHeaders
listener and updates layered in-memory overrides instead of stacking a new
listener for every channel change.
Rules:
- empty playlist-level
userAgentandrefererclear all active overrides - empty channel-level
userAgentandrefererwith ascopeUrlclear only the scoped channel override, preserving playlist-level defaults - channel playback should pass the stream URL as
scopeUrl - scoped overrides apply only to the active stream origin and referer origin
- playlist-level user agents and referrers may call the bridge without a
scopeUrl; that is intentionally broader because playlist settings apply to the whole M3U playlist - header names are replaced case-insensitively before canonical
User-Agent,Referer, andOriginnames are written
When changing this flow, keep stale header cleanup covered. Switching from a channel or playlist with custom headers to one without custom headers must clear the previous override.
Main-Process Remote Requests
Renderer-triggered HTTP requests must pass through the URL policy in
apps/electron-backend/src/app/events/url-safety.ts. The policy rejects
non-HTTP(S) URLs and embedded credentials, and strict callers also reject
loopback, private, reserved, and DNS-resolved private addresses. IPv4-mapped
IPv6 literals are decoded before classification, including hexadecimal forms
such as ::ffff:7f00:1, so alternate IPv6 spelling cannot bypass IPv4 rules.
Remote request callers must use the validated Axios redirect helper so every
redirect target is checked before the main process follows it. Under the strict
policy, the helper pins the socket lookup to the IP addresses that passed
validation while retaining the original hostname for TLS SNI, certificate
validation, and virtual hosting. This prevents DNS rebinding between validation
and connection. Callers with custom TLS policy provide a typed agent factory;
the validated request layer supplies the pinned lookup instead of copying
private Node Agent.options state. Credential stripping is scoped to the host
rather than the origin, with one transport-security carve-out: same-hostname
redirects that upgrade the scheme (http→https) or move ports keep
Authorization, Cookie, basic auth, params, and request bodies, because
IPTV portals routinely answer with such redirects and losing the session
cookie/token there breaks the portal outright (#1158) while disclosing nothing
to a third party. Redirects that change the host or downgrade https→http
must not forward any of those — the former would hand provider credentials to a
third party, the latter would replay a TLS-obtained session in cleartext.
EPG URLs are strict by default because an M3U playlist can supply them through
url-tvg. Operators who intentionally use a LAN-hosted EPG source should prefer
the renderer's source-scoped “Allow source” action, which persists the exact EPG
URL in settings and retries that source only. The
IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS=1 environment flag remains an
emergency/development process-wide override for strict EPG fetches. Directly
configured Xtream, Stalker, and playlist providers retain private-network
support, but still require HTTP(S), reject embedded credentials, and validate
redirects.
Callers that allow private-network provider URLs but only need redirects within
the same provider origin should pass both allowPrivateNetworkRedirects: false
and pinAllowedPrivateNetworkHosts: true to the validated Axios helper. This
keeps same-origin LAN redirects working, reuses the initially resolved addresses
for same-origin redirect hops, and requires cross-origin redirects to resolve to
public addresses. A provider-controlled URL cannot bounce the Electron main
process to another private or loopback host or rebind the same hostname between
redirect hops.
Remote playlist TLS certificates are validated by default. The
renderer can persist a host-scoped invalid-certificate trust decision for a
playlist or EPG source host. The IPTVNATOR_ALLOW_INSECURE_TLS=1 escape hatch
is only for explicitly trusted providers with invalid or self-signed
certificates when the host-scoped UI path is not available.
Sensitive Diagnostic Logging
Settings, portal requests/responses, IPC trace payloads, and remote-request
errors can contain provider credentials. Code at those boundaries must pass
structured values through redactSensitiveData from
@iptvnator/shared/logging, or through the portal createLogger/portal-debug
helpers that apply it. Do not send a raw settings object, request params,
response, or Error directly to console.*.
The redactor preserves non-sensitive diagnostic fields while replacing credential fields case-insensitively, including usernames, passwords, tokens, API keys, authorization/cookie headers, and MAC addresses. It also sanitizes URL query parameters, serialized JSON, nested query values, errors, arrays, and cyclic objects without mutating the original value. Depth, collection, object-key, and string limits keep opt-in debug traces bounded.
When adding a new logging boundary, extend the closest regression test with a synthetic secret and assert that the exact value is absent from captured log output. Never use a real provider credential to validate logging.
Filesystem Capabilities
Renderer IPC payloads are not filesystem authorization.
write-fileaccepts only a path returned to the same renderer by the native save dialog. The capability is single-use and is consumed before the write, including when the filesystem operation fails.- Download folders are owned by the Electron main process. The OS downloads directory is always allowed; a custom directory is accepted only after the native folder dialog selects it.
- The selected download directory is persisted under Electron
userDataand returned byDOWNLOADS_GET_DEFAULT_FOLDER, so renderer-managed settings cannot substitute an arbitrary host path. - Downloads do not overwrite an existing destination file.
- Reveal and playback handlers accept only file paths recorded in IPTVnator's downloads database.