Passa al contenuto principale

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 BrowserWindow separata 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

SuperficiePerché fidata
Il binary playroll-ui firmatoBuildato e code-signed dalla pipeline installer (DigiCert).
%LOCALAPPDATA%\Playroll\auth.jsonCifrato a riposo via Electron safeStorage (DPAPI su Windows), legato all'utente OS.
%LOCALAPPDATA%\Playroll\core-session-secret.txtScritto solo dalla UI; condiviso con il Core su un bind loopback.
La BrowserWindow principale dopo aver caricato l'index file:// impacchettatoSandboxed, context-isolated, servita esclusivamente dall'ASAR packaged.
Risposte da @supabase/supabase-js dopo che il JWT è stato validato da SupabaseL'SDK gestisce trasporto e validazione firma contro l'auth server del progetto.

Cosa consideriamo untrusted

SuperficieOriginePerché non fidata
Payload IPC dal rendererRenderer processDefense 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:8080Servizio Core localeLocale 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-IGDBServizio esterno della communityFuori 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 controlloAutenticato, 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:

DefenseDoveNote
Trusted-sender checksrc/main/ipc/trusted-sender.tsRifiuta 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 validationsrc/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 fallitaEvento OTEL ipc.validation_failedLogga 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 packagedPLAYROLL_INSECURE_DISABLE_IPC_SENDER_GUARDGated su !app.isPackaged.

Hardening specifici da segnalare:

  • SHOW_IN_FOLDER richiede che il path risolto stia sotto la recordings root dell'utente prima di chiamare shell.openPath / shell.showItemInFolder.
  • OPEN_EXTERNAL_URL permette solo schemi http://, https:// e steam://, parsati via WHATWG URL parser.
  • Gli input path passano per safePathSchema che 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:

DefenseDove
sandbox: true + contextIsolation: true + nodeIntegration: falsesrc/main/auth/auth-manager.ts:openSteamLogin
setWindowOpenHandler nega ogni window.open / target=_blankIdem
will-navigate e will-redirect allowlisted alla auth origin configurata più hostname Steam community/store/loginIdem
playroll://auth/callback?... accettato solo mentre un login flow è genuinamente pendinghandleAuthCallback / 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

DefenseDove
Secret condiviso per-sessione nell'header X-Api-Keysrc/main/core-lifecycle.ts:coreFetch
Secret rigenerato ad ogni install UI / first launchgetOrCreateCoreSessionSecret in src/main/index.ts
Env var di insecure-bypass ignorate in build packagedPLAYROLL_INSECURE_DISABLE_CORE_LOCAL_AUTH, gated su !app.isPackaged
Risposte /health, /ping, /shutdown gated da schemi zodsrc/shared/schemas/core-responses.ts
Tutte le altre risposte HTTP Core gated da uno schema minimal-strictapiCallValidated 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:

EndpointSchemaNote
Proxy metadati Playnite-IGDBIgdbMetadataEnvelopeSchemaI 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-updateResolveUpdateResponseSchemadownloadUrl 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

SuperficieStato
nodeIntegration nel rendererDisabilitato. Il renderer non può fare require() di moduli Node.
webSecurity: false / allowRunningInsecureContentNon settati da nessuna parte.
Caricamento di script remoti nella finestra principaleNon usato. Il renderer è servito da file:// (produzione) con CSP strict.
child_process spawn controllato dal rendererNon esposto. L'IPC di launch / focus accetta solo un game id, cercato nella library locale rilevata.
Tag webviewBloccato all'evento web-contents-created.
Richieste permission mic / camera / geolocation / USB / serialTutte 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.