Trust boundary degli input
Questa pagina documenta i confini di fiducia del Playroll Core (playroll-core.exe) e la validazione applicata su ciascuno di essi. È pensata per chi rivede le pull request, per chi conduce audit di sicurezza e per chi integra la API HTTP locale.
Il Core è un servizio di background per Windows che:
- Accetta JSON su
http://127.0.0.1:8080da un singolo client locale fidato (la UI Electron). - Legge il catalogo dei giochi da Supabase come JSON.
- Legge file di configurazione del motore di gioco (
.ini,.xml) sotto%APPDATA%per i giochi rilevati. - Scrive le registrazioni sotto
%USERPROFILE%\Videos\PlayrollCaptures(o un percorso configurato dall'utente). - Carica le registrazioni completate su S3 tramite URL pre-firmati di breve durata ottenuti da una Edge Function di Supabase.
Il Core non accetta traffico di rete dall'esterno dell'interfaccia loopback, non invoca processi esterni a partire da input controllato dall'utente, e non incorpora alcun motore di scripting.
Cosa consideriamo fidato
| Superficie | Perché fidato |
|---|---|
Il binario firmato playroll-core.exe | Costruito e firmato dalla pipeline dell'installer (DigiCert). |
Valori di registro HKCU\Software\Playnite\PlayRoll\* | Scritti solo dall'installer / dalla UI sulla stessa macchina. |
File sotto %LOCALAPPDATA%\Playroll\* (token di autenticazione, session secret, DB SQLite) | Scritti solo dal Core / dalla UI sulla stessa macchina. Le ACL di default sono quelle del profilo utente. |
La UI Electron quando presenta un session secret valido su 127.0.0.1:8080 | Segreto condiviso stabilito all'installazione; vedi Raccolta dati. |
| Dipendenze vcpkg hard-coded | Bloccate dal baseline del manifest + hash del manifest vcpkg. |
Cosa consideriamo non fidato
| Superficie | Origine | Perché non fidato |
|---|---|---|
| Body delle richieste HTTP | UI Electron locale (dopo l'auth con session secret) | Difesa in profondità: un'estensione malevola o una UI compromessa non deve poter corrompere il Core. |
| Catalogo giochi Supabase | Tabella games / Edge Function | Il contenuto del database è controllato lato server ma entra nel processo C++ come JSON; trattato come non fidato per mantenere il Core robusto contro dati corrotti, drift di schema o future lacune di moderazione dei contenuti. |
File di configurazione del motore di gioco (.ini, .xml) sotto %APPDATA%\<Game>\ | Scritti dal gioco stesso | I file dei giochi non sono sotto il nostro controllo; li leggiamo solo per sondare le impostazioni di risoluzione. |
| Query string HTTP e parametri di path | UI locale | Stessa motivazione dei body. |
Superfici di input e difese attuali
API HTTP
Tutte le route sono definite in src/service/http_server.cpp e nei file *_http_callbacks.cpp. Difese:
| Difesa | Dove | Note |
|---|---|---|
| Bind solo su loopback | http_server.cpp | Il listener è su 127.0.0.1:8080; il traffico LAN viene rifiutato a livello di socket. |
| Autenticazione con session secret | requireSessionSecret() | Ogni endpoint che modifica stato richiede il segreto da %LOCALAPPDATA%\Playroll\core-session-secret.txt. |
| Bearer JWT per le chiamate autenticate | requireAuth() | Valida la presenza del token e il suo hint di scadenza locale. |
| Parsing JSON racchiuso in try/catch | handleJsonRequest() | JSON malformato restituisce 400 ed è loggato come http.invalid_json (con throttling). |
| Estrazione tipata con fallback | jsonGet<T>() | Campi mancanti o con tipo sbagliato ricadono su un valore di default, senza mai propagare eccezioni fuori dall'handler. |
| Cap sulla lunghezza delle stringhe | jsonGetStringCapped() in include/util/json_validation.hpp | Applicato ai campi JWT (8 KB), agli ID encoder (128 B), agli identificatori di lobby (256 B). I valori oversize vengono rifiutati come se mancassero. |
| Clamp dei range numerici | jsonGetClamped<T>() in include/util/json_validation.hpp | Helper disponibile per qualunque futuro campo numerico del body che governi sizing o allocazione. |
| Enum su whitelist | /api/uploader/action, /api/encoders/select | Verbi di azione e ID encoder sono validati contro liste fisse prima di raggiungere il codice della macchina a stati. |
Catalogo giochi Supabase
Il parsing è in src/service/game_catalog_parser.cpp. Difese:
| Difesa | Dove |
|---|---|
.value(key, default) e check .is_array() invece di .get<T>() nudo | parseSupabaseGameCatalog() |
display_name viene sanitizzato prima di essere usato come nome cartella | src/service/recording_paths.cpp via util::sanitizeFilenameComponent |
Boundary check con weakly_normal sul path di registrazione finale | src/service/recording_paths.cpp via util::pathIsWithin |
| Comportamento del sanitizer — vedi la sezione successiva | src/util/path_sanitize.cpp |
Path derivati da input esterno
Un singolo helper governa ogni componente di un path di registrazione che proviene dall'esterno del Core: util::sanitizeFilenameComponent in include/util/path_sanitize.hpp. È intenzionalmente un sanitizer di un singolo componente (non di path multi-segmento) e produce risultati che:
- Non contengono separatori di path (
/,\), separatori di drive (:), wildcard (*,?), virgolette ("), parentesi angolari (<,>), pipe (|) o caratteri di controllo ASCII. - Non sono mai uguali a un nome di device riservato Windows (
CON,PRN,AUX,NUL,COM1..COM9,LPT1..LPT9), nemmeno con un'estensione. - Non hanno punti o spazi iniziali o finali (che Windows rimuove silenziosamente).
- Sono limitati a 100 caratteri wide.
- Non sono mai vuoti (restituisce
unnamedper un input che si sanitizza a vuoto).
Dopo la sanitizzazione, recording_paths.cpp ricostruisce il path completo e verifica con util::pathIsWithin che il risultato sia un discendente stretto della root delle registrazioni dell'utente. Se non lo è (bug del sanitizer o regressione futura), ricade su un nome letterale _unsafe_game_folder ed emette l'evento di log recording.unsafe_game_folder.
File di configurazione dei motori di gioco
src/service/resolution_probe.cpp parsa file XML e JSON scritti dai giochi. Il parser:
- Carica file solo da path derivati da
%PROGRAMFILES%,%USERPROFILE%o%LOCALAPPDATA%dopoExpandEnvironmentStringsWe un checklooksResolved()che rifiuta stringhe con placeholder%TOKEN%non risolti. - Usa
pugixmlper l'XML e lo stesso pattern.find()/.is_object()/.is_string()per il JSON.
Questi file provengono da giochi installati sulla macchina dell'utente stesso, quindi il threat model assume un produttore benigno-ma-buggato anziché un attaccante.
Query SQL
Tutti gli accessi a SQLite passano da src/service/sqlite_state.cpp. Ogni query usa sqlite3_prepare_v2() con parametri sqlite3_bind_*() — non c'è concatenazione di stringhe SQL da nessuna parte nel Core.
Superfici del sistema operativo che non esponiamo
| Superficie | Stato |
|---|---|
CreateProcess / ShellExecute / system da input utente | Non usati. La pipeline di cattura inizializza NVENC e il muxer ffmpeg in-process solo via chiamate di libreria. |
| Caricamento di DLL fornite dall'utente | Non usato. |
| Motore di scripting incorporato (Lua, JS, ...) | Non presente. |
| Traffico in ingresso non-loopback | Non in bind. |
Cosa deliberatamente non distribuiamo
libx264non è linkato. L'encoding H.264 passa direttamente dal Video SDK NVIDIA (NVENC); il muxer fa riferimento aAV_CODEC_ID_H264solo come costante.- Le feature
ffmpeg[nonfree](es.libfdk-aac,libnpp) non sono abilitate. L'encoding AAC è fatto da Windows Media Foundation; nessun codec nonfree finisce nel binario. spdlogefmtnon sono linkati. Il logger è implementato direttamente soprastd::ofstreamenlohmann::json.
Queste esclusioni riducono la superficie di attacco e mantengono il binario ridistribuibile sotto i termini LGPL.
Segnalare una vulnerabilità
Inviare segnalazioni di sicurezza a security@extrinsiccognition.com. Includere:
- Una descrizione chiara del problema e dell'impatto presunto.
- Passi per riprodurlo, idealmente contro una build locale di
playroll-core.exe. - L'etichetta di versione restituita da
GET http://127.0.0.1:8080/status.
L'obiettivo è confermare la ricezione entro due giorni lavorativi. Non aprire issue di sicurezza come issue pubbliche su GitHub.