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

319 lines
12 KiB
Markdown

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