Files
RUWIO/dev_docs/03_security-remediation-plan.md
2026-06-07 11:10:05 +02:00

12 KiB

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 <token> 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.

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.