Trust boundary della UI
Questa pagina documenta i confini di fiducia della UI desktop Playroll (playroll-ui, applicazione Electron) e la validazione applicata su ciascuno di essi. È la controparte renderer / main-process di Trust boundary degli input, che copre il servizio C++ Core.
La UI è l'unico software dello stack Playroll che:
- Esegue codice renderer da
file://(produzione) o dal Vite dev server (sviluppo), con sandbox Electron stretto e context isolation attivi. - Gestisce il flow Steam OpenID OAuth in una
BrowserWindowseparata con la propria session partition. - Parla con l'API HTTP localhost esposta da
playroll-core.exe, autenticata con un secret per-sessione. - Parla con Supabase dal main process (il renderer non vede mai service-role o access token).
- Recupera le cover artwork da un proxy esterno per i metadati Playnite-IGDB.
- Risolve e scarica i propri update installer da una Supabase Edge Function e da una CDN releases HTTPS.
NON espone nodeIntegration al renderer, NON abilita webSecurity: false o allowRunningInsecureContent, NON accetta traffico di rete inbound, e NON carica script remoti nella finestra principale.
Cosa consideriamo trusted
| Superficie | Perché fidata |
|---|---|
Il binary playroll-ui firmato | Buildato e code-signed dalla pipeline installer (DigiCert). |
%LOCALAPPDATA%\Playroll\auth.json | Cifrato a riposo via Electron safeStorage (DPAPI su Windows), legato all'utente OS. |
%LOCALAPPDATA%\Playroll\core-session-secret.txt | Scritto solo dalla UI; condiviso con il Core su un bind loopback. |
La BrowserWindow principale dopo aver caricato l'index file:// impacchettato | Sandboxed, context-isolated, servita esclusivamente dall'ASAR packaged. |
Risposte da @supabase/supabase-js dopo che il JWT è stato validato da Supabase | L'SDK gestisce trasporto e validazione firma contro l'auth server del progetto. |
Cosa consideriamo untrusted
| Superficie | Origine | Perché non fidata |
|---|---|---|
| Payload IPC dal renderer | Renderer process | Defense in depth: un renderer compromesso (es. via XSS-equivalent in una stringa renderizzata) non deve poter corrompere il main process. |
Risposte da playroll-core su 127.0.0.1:8080 | Servizio Core locale | Locale ma boundary di processo separato; la UI non assume fedeltà di schema da una versione Core potenzialmente disallineata. |
Deep link playroll://auth/callback?... | Qualsiasi cosa sull'host capace di invocare l'handler di protocollo registrato (browser, mail client, altre app) | Una vittima che clicca un link malevolo nel browser non deve poter sovrascrivere la sessione salvata dell'utente. |
| URL cover/artwork dal proxy metadati Playnite-IGDB | Servizio esterno della community | Fuori dal nostro controllo; le URL finiscono direttamente in renderer <img src>. |
Risposta della edge function resolve-update (download URL + SHA-256) | Progetto Supabase sotto il nostro controllo | Autenticato, ma security-critical perché downloadUrl arriva al downloader installer. |
File preferenze su disco (preferences.json) | L'utente (o qualsiasi processo che gira come utente) | I valori path finiscono in shell.openPath; un file modificato non deve poter uscire dalla recordings root. |
Defense per superficie
IPC main ↔ renderer
Tutti i canali passano per un singolo helper di registrazione che combina due guard:
| Defense | Dove | Note |
|---|---|---|
| Trusted-sender check | src/main/ipc/trusted-sender.ts | Rifiuta IPC da qualsiasi webContents la cui URL non sia il renderer entry point configurato (produzione file:// o dev URL Vite). Anche IPC da subframe rifiutato. |
| Strict schema validation | src/main/ipc/validated-handler.ts + src/shared/schemas/ | Ogni canale con payload è registrato con uno schema zod. Chiavi sconosciute, tipi sbagliati, numeri fuori range, stringhe oversized, chiavi di prototype-pollution e path traversal sono rifiutati al boundary. |
| Telemetria su validation fallita | Evento OTEL ipc.validation_failed | Logga il nome del canale e zod issue paths solo — mai il payload raw (evita leak di token o PII). |
| Env var di insecure-bypass ignorate in build packaged | PLAYROLL_INSECURE_DISABLE_IPC_SENDER_GUARD | Gated su !app.isPackaged. |
Hardening specifici da segnalare:
SHOW_IN_FOLDERrichiede che il path risolto stia sotto la recordings root dell'utente prima di chiamareshell.openPath/shell.showItemInFolder.OPEN_EXTERNAL_URLpermette solo schemihttp://,https://esteam://, parsati via WHATWG URL parser.- Gli input path passano per
safePathSchemache rifiuta traversal del parent, NUL byte, UNC path, e schemi URL embedded. - Gli input free-text hanno bound espliciti; gli oggetti sono
.strict()(chiavi extra rifiutate).
Flow OAuth Steam
Il popup Steam login gira nella propria BrowserWindow con session partition dedicata:
| Defense | Dove |
|---|---|
sandbox: true + contextIsolation: true + nodeIntegration: false | src/main/auth/auth-manager.ts:openSteamLogin |
setWindowOpenHandler nega ogni window.open / target=_blank | Idem |
will-navigate e will-redirect allowlisted alla auth origin configurata più hostname Steam community/store/login | Idem |
playroll://auth/callback?... accettato solo mentre un login flow è genuinamente pending | handleAuthCallback / handleAuthError |
| Stato pending consumato al primo callback (no replay) | Idem |
| Finestra pending bounded (5 minuti) | Idem |
Insieme bloccano il percorso "vittima clicca link malevolo nel browser" di session-hijack: senza una openSteamLogin attiva il callback è rifiutato con No login in progress.
API localhost playroll-core
| Defense | Dove |
|---|---|
Secret condiviso per-sessione nell'header X-Api-Key | src/main/core-lifecycle.ts:coreFetch |
| Secret rigenerato ad ogni install UI / first launch | getOrCreateCoreSessionSecret in src/main/index.ts |
| Env var di insecure-bypass ignorate in build packaged | PLAYROLL_INSECURE_DISABLE_CORE_LOCAL_AUTH, gated su !app.isPackaged |
Risposte /health, /ping, /shutdown gated da schemi zod | src/shared/schemas/core-responses.ts |
| Tutte le altre risposte HTTP Core gated da uno schema minimal-strict | apiCallValidated in src/main/ipc/handlers.ts |
L'approccio minimal-strict + passthrough (vedi docs/security/input-trust-boundaries.md per la controparte Core) è deliberato: lo schema rifiuta risposte non-oggetto al boundary, e i normalizer di dominio continuano a fare alias mapping e fallback shaping. Ogni livello fa quello in cui è bravo.
Risposte external network
I due endpoint fuori dal nostro controllo diretto:
| Endpoint | Schema | Note |
|---|---|---|
| Proxy metadati Playnite-IGDB | IgdbMetadataEnvelopeSchema | I campi URL cover e artwork sono pinned all'host images.igdb.com; URL con javascript:, data:, file: e altri schemi pericolosi sono rifiutati prima di arrivare a renderer <img src>. Envelope strict (no passthrough) — campi sconosciuti del proxy non possono leakare nello stato UI. |
Edge function Supabase resolve-update | ResolveUpdateResponseSchema | downloadUrl strict HTTPS-only. installerSha256 strict 64-char hex. Enum chiusi per reason e assignment.track. Envelope strict. |
L'installer scaricato è verificato contro lo SHA-256 di questa risposta prima di essere lanciato. La verifica della firma Authenticode è un follow-up pianificato; il gate SHA è il controllo primario di integrità oggi.
Notifiche server-driven Supabase
Le rows join da notification_state + notification_event sono gated da ServerNotificationRowSchema prima di essere pushate al renderer. Una row malformata o malevola viene scartata silenziosamente invece di arrivare allo stato React.
File preferenze su disco
recordingsPath da %APPDATA%/Roaming/playroll-ui/preferences.json è validato via safePathSchema (no traversal del parent, NUL, UNC, schemi URL) prima di essere passato a shell.openPath o mkdir. Il layer di store runtime (gate-store.ts, preferences-store.ts) mantiene il proprio sanitizer field-by-field difensivo come defense in depth contro corruption on-disk.
Superfici che deliberatamente NON esponiamo
| Superficie | Stato |
|---|---|
nodeIntegration nel renderer | Disabilitato. Il renderer non può fare require() di moduli Node. |
webSecurity: false / allowRunningInsecureContent | Non settati da nessuna parte. |
| Caricamento di script remoti nella finestra principale | Non usato. Il renderer è servito da file:// (produzione) con CSP strict. |
child_process spawn controllato dal renderer | Non esposto. L'IPC di launch / focus accetta solo un game id, cercato nella library locale rilevata. |
Tag webview | Bloccato all'evento web-contents-created. |
| Richieste permission mic / camera / geolocation / USB / serial | Tutte negate via setPermissionRequestHandler. |
Segnalare una vulnerabilità
Invia report di sicurezza a security@extrinsiccognition.com. Includi:
- Descrizione chiara del problema e dell'impatto che pensi abbia.
- Passi per riprodurlo, idealmente contro una build debug locale di
playroll-ui. - La versione mostrata nel footer del launcher o via IPC
GET_VERSION.
Cerchiamo di confermare la ricezione entro due giorni lavorativi. Non aprire issue di sicurezza come issue GitHub pubbliche.