# Security Review Date: 2026-06-05 Scope reviewed: - Vue/Vite renderer in `src/` - Electron main/preload in `electron/` - MP4 render server in `server/` - Project import/export paths for `.json`, `.ruw`, backups, assets, fonts, and generated HTML exports - Build/dependency posture at the repository root ## Summary The desktop shell starts from a good Electron baseline: `contextIsolation: true`, `nodeIntegration: false`, `sandbox: true`, and a narrow preload bridge. I did not find direct Node exposure to the Vue renderer. The highest-risk area is the standalone MP4 render server. It accepts unauthenticated cross-origin render jobs, large request bodies, and attacker-controlled scene data, then renders it in Chromium with `--no-sandbox` and feeds frames to `ffmpeg`. That combination should be treated as a network-exposed compute/sandbox boundary until it is tightened. The second broad theme is input hardening. Project files and `.ruw` bundles are structurally checked, but not deeply schema-validated or bounded before the app uses canvas sizes, frame counts, assets, and timelines. A crafted file can likely cause heavy CPU/memory use in both browser and server render paths. ## Findings ### High: Render Server Is Unauthenticated And Cross-Origin Evidence: - `server/server.js:19` enables default `cors()`, allowing browser requests from any origin. - `server/server.js:20` accepts JSON bodies up to `200mb`. - `server/server.js:27` starts render jobs without authentication, CSRF protection, origin checks, rate limits, or concurrency limits. - `docker-compose.yml:4-5` publishes port `6000` on the host. Impact: Any site reachable by a user's browser, or any network client that can reach the service, can submit expensive render jobs. This can exhaust CPU, memory, disk, Chromium processes, or `ffmpeg` work queues. If the server is ever exposed beyond localhost, it becomes a public unauthenticated render farm. Recommendations: - Bind the service to localhost by default, or require an explicit opt-in for network exposure. - Replace default `cors()` with an allowlist for the app origin. - Add authentication or a per-session token for `/render`, `/events`, `/file`, and `DELETE`. - Add request rate limits, maximum active jobs, queue limits, and per-job timeouts. - Lower the default body limit and reject requests before parsing when possible. ### High: Chromium Renders Untrusted Data With Sandbox Disabled Evidence: - `server/server.js:126-134` launches Chromium with `--no-sandbox` and `--disable-setuid-sandbox`. - `server/server.js:166-167` loads `render.html` and injects caller-provided scene data into the page. - `server/Dockerfile:1-20` uses the default root user; no non-root `USER` is configured. Impact: The server renders attacker-controlled image/audio/video/SVG-like asset data in Chromium. If a browser engine, image decoder, font decoder, media decoder, or canvas path is exploited, disabling the Chromium sandbox and running the container as root increases the blast radius. Recommendations: - Run the container as a non-root user. - Prefer enabling the Chromium sandbox. If the container needs extra setup, document and configure it rather than relying on `--no-sandbox`. - Treat each render job as untrusted: use process-level timeouts, memory/CPU limits, and container hardening. - Consider a separate locked-down worker container for renders. ### Medium: Project And Bundle Imports Lack Deep Validation And Bounds Evidence: - `src/lib/projectFile.ts:39-67` checks only top-level `magic`, `canvas`, `scenes`, and `assets` shape before returning the imported object as `Project`. - `src/lib/zipUtil.ts:121-171` parses ZIP entries into memory without explicit caps on entry count, central directory size, total extracted size, or path/name length. - `server/server.js:115-119` derives `fps`, `duration`, and `totalFrames` directly from request data. - `server/server.js:135-138` uses request-provided canvas dimensions as the Chromium viewport. Impact: A crafted `.json`, `.ruw`, backup, or server request can specify extreme canvas sizes, frame counts, layer/keyframe arrays, base64 assets, font data, or ZIP metadata. The likely outcome is denial of service: browser tab hangs, Electron renderer memory pressure, server CPU exhaustion, or failed/very slow exports. Recommendations: - Add a real project schema validator at file and server boundaries. - Clamp canvas dimensions, duration, fps, layer counts, keyframe counts, asset count, asset byte size, and total project size. - Add ZIP reader bounds and clearer errors for malformed/truncated archives. - Normalize imported projects into a safe internal shape instead of trusting arbitrary object fields. ### Medium: Render Server Dependency Installs Are Not Reproducible Evidence: - `server/package.json` exists, but no `server/package-lock.json` is present. - `server/Dockerfile:15-16` copies only `package.json` and runs `npm install --omit=dev`. - `npm audit --prefix server --omit=dev` cannot run without a lockfile. Impact: Server builds may pull different transitive dependency versions over time. That makes vulnerability tracking, incident response, and reproducible releases harder. Recommendations: - Commit `server/package-lock.json`. - Use `npm ci --omit=dev` in the server Dockerfile. - Add server dependency auditing to CI. ### Medium: Development Test Overlay Ships In The Production Bundle Evidence: - `src/App.vue:38` statically imports `./devtools/TestOverlay.vue`. - `src/App.vue:806` always mounts the component. - `npm run build` output shows `src/devtools/tests.ts` pulled into the production bundle through static/dynamic import relationships. Impact: This increases production attack surface and bundle size. Even if the test runner is intended to open only from a development-only Electron menu item, the code is still shipped to every web and desktop user. Recommendations: - Gate the test overlay behind `import.meta.env.DEV`. - Load it with `defineAsyncComponent` or dynamic import only in development. - Ensure production builds do not include `src/devtools/tests.ts`. ### Low: Electron Navigation And External Link Policy Could Be Stricter Evidence: - `electron/main.cts:40-44` opens any `http://` or `https://` URL externally. - `electron/main.cts:47-51` uses string `startsWith` checks for navigation allow/deny decisions. Impact: This is not a direct Node compromise because the renderer is isolated. Still, if untrusted content ever reaches a clickable/openable path, arbitrary external links can be launched. String-prefix navigation checks are also easier to get wrong than parsed URL origin/path checks. Recommendations: - Prefer parsed URL checks over `startsWith`. - Allow only expected origins in development. - Consider allowing only `https:` external links, or an explicit domain allowlist for help/about links. ### Low: No Content Security Policy Is Defined Evidence: - `index.html` and built `dist/index.html` have no CSP meta tag. Impact: Vue's default escaping reduces XSS risk, and Electron disables Node integration. A CSP would still provide defense in depth for the web app, generated asset previews, and Electron renderer by limiting script, media, image, and connection destinations. Recommendations: - Add a CSP for the app shell. - Account for required `data:`/`blob:` sources for user assets and exports. - Add Electron-specific CSP validation during packaged smoke tests. ## Positive Notes - Electron uses `contextIsolation`, disables `nodeIntegration`, enables renderer sandboxing, and exposes only a small preload API. - The preload bridge does not expose arbitrary IPC invocation or Node primitives. - The Electron app denies `window.open` in-app and prevents most unexpected navigation. - `.ruw` uses STORE-only ZIP handling, avoiding decompression bombs from deflated entries. - The root production dependency audit completed with `0 vulnerabilities`. - A shallow secret scan found no obvious committed `.env`, key, secret, PEM, or certificate files outside dependencies. ## Verification Performed - `npm run build` passed. - `npm audit --omit=dev` passed with `0 vulnerabilities`. - `npm audit --prefix server --omit=dev` could not run because `server/` has no lockfile. ## Suggested Remediation Order 1. Lock down the render server: localhost binding, CORS allowlist, auth/token, rate limits, job limits, and timeouts. 2. Run render workers as non-root and remove Chromium `--no-sandbox` if feasible. 3. Add schema validation and resource bounds for imported projects, `.ruw` bundles, backups, and render requests. 4. Add `server/package-lock.json`, switch Docker to `npm ci --omit=dev`, and audit server dependencies in CI. 5. Remove devtools/test runner code from production bundles. 6. Add CSP and stricter Electron URL parsing/allowlists.