# Security Remediation Plan Date: 2026-06-05 Source review: `dev_docs/02_security-review.md` ## Goal Close the security issues from the review without changing RUWIO's core authoring workflow. The first priority is to make the MP4 render server safe to run on a developer machine by default; the second is to make project and bundle input handling bounded and reproducible. ## Principles - Secure defaults first: local-only render server, explicit opt-in for network exposure, and bounded work. - Validate at every trust boundary: imported files, `.ruw` bundles, render requests, Electron IPC, and generated/exported HTML. - Keep browser and desktop builds working while hardening server-only paths. - Add tests for security behavior so fixes do not drift quietly. ## Phase 0: Baseline And Guardrails Files likely touched: - `dev_docs/02_security-review.md` - `package.json` - `server/package.json` - `server/server.js` - `src/devtools/tests.ts` Tasks: 1. Capture a clean baseline with `npm run build`. 2. Add or document a lightweight verification command for render-server checks once server tests exist. 3. Decide the initial production limits so later validation has concrete numbers: - maximum canvas width and height - maximum render fps - maximum render duration and total frames - maximum layer, clip, keyframe, scene, asset, and point counts - maximum JSON body, `.json` project, `.ruw` bundle, per-ZIP-entry, and total extracted asset size 4. Record the selected limits in this document or a small constants module. Acceptance: - Existing build still passes before remediation starts. - Limits are explicit instead of scattered magic numbers. ## Phase 1: Lock Down The Render Server Addresses: - High: unauthenticated cross-origin render server - Part of Medium: unbounded render requests Files likely touched: - `server/server.js` - `src/lib/serverExport.ts` - `src/components/ExportDialog.vue` - `docker-compose.yml` - `README.md` Tasks: 1. Bind the server to `127.0.0.1` by default: - Add `HOST=127.0.0.1` default in `server/server.js`. - Use `app.listen(PORT, HOST, ...)`. - Change Docker Compose publishing from `6000:6000` to `127.0.0.1:6000:6000`. - Require an explicit `HOST=0.0.0.0` or compose override for LAN exposure. 2. Replace default `cors()` with an allowlist: - Add `RENDER_ALLOWED_ORIGINS`, defaulting to the Vite dev origin and localhost app origins that are actually needed. - Reject browser requests with unexpected `Origin` values. - Keep non-browser/local CLI requests possible when they have no `Origin` and pass auth. 3. Add authentication: - Add `RENDER_TOKEN`. - Require `Authorization: Bearer ` or `X-RUWIO-Render-Token` for `POST /render`. - Because `EventSource` cannot send custom headers, have `POST /render` return a random per-job access token, then require that token for `/render/:id/events`, `/render/:id/file`, and `DELETE /render/:id`. - Add render-server settings in the client if the token must be user-configurable. 4. Add basic abuse controls: - Lower `express.json` from `200mb` to a documented default, with an environment override. - Reject requests with a too-large `Content-Length` before JSON parsing. - Add per-IP request rate limits for `/render`. - Add `MAX_ACTIVE_JOBS` and `MAX_QUEUED_JOBS`; return `429` or `503` when full. - Add a per-job timeout that aborts Puppeteer and `ffmpeg`. 5. Improve job cleanup: - Delete failed and aborted job directories. - Remove completed job metadata after download TTL expires. - Ensure `ffmpeg` is killed when Chromium/page work fails. Acceptance: - A default server only listens on localhost. - Cross-origin requests from an unlisted origin fail. - Requests without the configured token fail. - A completed job can still be started, watched, downloaded, and deleted by the RUWIO client. - Concurrent jobs, over-large bodies, and over-long renders are rejected with clear errors. ## Phase 2: Harden Chromium And The Container Addresses: - High: Chromium renders untrusted data with sandbox disabled Files likely touched: - `server/server.js` - `server/Dockerfile` - `docker-compose.yml` - `README.md` Tasks: 1. Run the container as a non-root user: - Create an app user/group in `server/Dockerfile`. - Ensure `/app` and temporary render directories are writable by that user. - Set `USER` before `CMD`. 2. Remove unconditional sandbox-disabling Chromium flags: - Try launching Chromium without `--no-sandbox` and `--disable-setuid-sandbox`. - Keep an explicit `CHROME_NO_SANDBOX=1` escape hatch only if needed for a known deployment environment. - Log a loud warning when the escape hatch is used. 3. Add process and container limits: - Configure Docker Compose memory/CPU/pids limits where supported. - Add Node-side child-process kill paths for abort, timeout, and client disconnect. - Consider using a dedicated worker module/process so Express request handling and rendering are isolated. 4. Limit network reachability from render jobs: - Prefer rendering local/data assets only. - If external URLs must remain supported, document the SSRF/privacy tradeoff and add an allowlist or opt-in later. Acceptance: - The Docker image runs as a non-root user. - The normal render path works without `--no-sandbox`, or the documented fallback is explicit and visible. - Aborted/timed-out jobs leave no running Chromium or `ffmpeg` process. ## Phase 3: Add Shared Project Validation And Resource Bounds Addresses: - Medium: project and bundle imports lack deep validation and bounds - Part of High/Medium: unbounded render workload Files likely touched: - `src/types/scene.ts` - new `src/lib/projectLimits.ts` - new `src/lib/projectValidation.ts` - `src/lib/projectFile.ts` - `src/lib/ruwFile.ts` - `src/lib/zipUtil.ts` - `server/server.js` - possibly `server/validation.js` or a shared package if server reuse becomes necessary Tasks: 1. Add centralized limits: - Keep numeric bounds in one module. - Use browser-safe limits for imports and stricter server-safe limits for rendering if needed. 2. Add schema validation at import boundaries: - Validate `Project`, `Scene`, `Asset`, `Layer`, `Keyframe`, clips, actions, background media, and canvas data. - Reject non-finite numbers, negative dimensions, invalid colors where practical, oversized strings, and unknown critical shapes. - Normalize optional arrays and legacy fields into safe defaults. - Return friendly `ProjectFileError` messages rather than raw parser errors. 3. Add render-request validation: - Validate `scene` and `options` before creating a job. - Clamp or reject `fps`, `duration`, `crf`, `preset`, canvas size, and total frame count. - Compute `totalFrames` only after validation. 4. Harden ZIP parsing: - Add caps for entry count, archive byte length, central directory range, name length, per-entry size, and total extracted size. - Check every local/central directory offset and data range before creating `DataView` or `subarray`. - Reject path traversal and absolute paths even though extraction is in-memory. - Preserve STORE-only behavior. 5. Add import tests: - Valid legacy `.json` and `.ruw` still load. - Malformed JSON reports a friendly error. - Oversized canvas, extreme fps/duration/keyframes/assets, malformed ZIP offsets, too many entries, and missing assets are rejected. Acceptance: - Project import returns a safe internal `Project` shape. - `.ruw` parsing fails early for oversized or malformed archives. - Render server rejects pathological render requests before launching Chromium. - Existing normal projects still open and export. ## Phase 4: Make Server Dependencies Reproducible Addresses: - Medium: render server dependency installs are not reproducible Files likely touched: - `server/package-lock.json` - `server/Dockerfile` - root CI config when one exists - `README.md` Tasks: 1. Generate and commit `server/package-lock.json`. 2. Replace `npm install --omit=dev` with `npm ci --omit=dev` in `server/Dockerfile`. 3. Add server audit/documentation: - `npm audit --prefix server --omit=dev` - Add the command to local release checks or CI. 4. Rebuild the server image from a clean checkout to verify the lockfile path. Acceptance: - `npm audit --prefix server --omit=dev` can run. - Docker builds use the committed lockfile. - Repeated builds install identical dependency versions. ## Phase 5: Remove Devtools From Production Bundles Addresses: - Medium: development test overlay ships in production Files likely touched: - `src/App.vue` - `src/devtools/TestOverlay.vue` - `src/devtools/tests.ts` Tasks: 1. Replace the static `TestOverlay` import with a development-only dynamic import: - Use `defineAsyncComponent` only when `import.meta.env.DEV` is true. - Mount the overlay only in development. 2. Guard menu/command paths that open the test runner: - Keep Electron menu gating already present. - Ensure renderer-side command handling ignores `test-runner` in production. 3. Verify bundle output: - Run `npm run build`. - Confirm production chunks do not include `src/devtools/tests.ts` or the test overlay. Acceptance: - Production build output excludes devtools tests. - Development build still opens the test overlay. ## Phase 6: Tighten Electron Navigation And External Links Addresses: - Low: Electron navigation and external link policy could be stricter Files likely touched: - `electron/main.cts` Tasks: 1. Replace string-prefix URL checks with parsed `URL` checks. 2. Define allowed renderer origins: - Development: exactly the configured Vite origin. - Production: the packaged `file://` app entry and same-directory app assets. 3. Restrict external opening: - Prefer `https:` only. - Add an explicit allowlist for help/about domains if external links are needed. - Deny `file:`, custom protocols, `javascript:`, and unexpected `http:`. 4. Add small unit or smoke tests if Electron test infrastructure exists; otherwise document manual packaged-app checks. Acceptance: - App navigation works in dev and packaged builds. - Unexpected app navigation is prevented. - External links are opened only when they match the policy. ## Phase 7: Add Content Security Policy Addresses: - Low: no Content Security Policy is defined Files likely touched: - `index.html` - possibly Vite config if separate dev/prod CSP handling is needed - generated export code only if app-shell CSP breaks previews Tasks: 1. Add an app-shell CSP meta tag. 2. Start with a policy that supports current app behavior: - scripts from self - images/media/fonts from self plus `data:`/`blob:` where required for user assets - connections to self plus configured render server during development/export - no object/embed 3. Verify the Electron renderer and browser app do not log CSP violations for normal workflows. 4. Keep generated HTML exports separate if they need a looser asset policy; do not silently weaken the app shell for exports. Acceptance: - App loads in browser dev, production preview, and Electron. - Common import/export/preview workflows work without unexpected CSP violations. - CSP blocks inline or remote script execution in the app shell. ## Suggested Pull Request Order 1. Render server exposure and authentication. 2. Render server job limits, body limits, and cleanup. 3. Container non-root user and Chromium sandbox work. 4. Shared limits plus render-request validation. 5. Project and `.ruw` import validation. 6. Server lockfile and Docker `npm ci`. 7. Production devtools gating. 8. Electron URL policy. 9. CSP. ## Final Verification Checklist - `npm run build` - `npm audit --omit=dev` - `npm audit --prefix server --omit=dev` - Render server rejects unauthenticated `/render`. - Render server rejects disallowed `Origin`. - Render server rejects oversized request bodies and excessive frame counts. - Normal MP4 render still completes through the app. - Malformed `.json` and `.ruw` files fail with friendly errors. - Production bundle does not contain `src/devtools/tests.ts`. - Packaged Electron app opens only expected app URLs and approved external links.