12 KiB
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,
.ruwbundles, 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.mdpackage.jsonserver/package.jsonserver/server.jssrc/devtools/tests.ts
Tasks:
- Capture a clean baseline with
npm run build. - Add or document a lightweight verification command for render-server checks once server tests exist.
- 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,
.jsonproject,.ruwbundle, per-ZIP-entry, and total extracted asset size
- 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.jssrc/lib/serverExport.tssrc/components/ExportDialog.vuedocker-compose.ymlREADME.md
Tasks:
- Bind the server to
127.0.0.1by default:- Add
HOST=127.0.0.1default inserver/server.js. - Use
app.listen(PORT, HOST, ...). - Change Docker Compose publishing from
6000:6000to127.0.0.1:6000:6000. - Require an explicit
HOST=0.0.0.0or compose override for LAN exposure.
- Add
- 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
Originvalues. - Keep non-browser/local CLI requests possible when they have no
Originand pass auth.
- Add
- Add authentication:
- Add
RENDER_TOKEN. - Require
Authorization: Bearer <token>orX-RUWIO-Render-TokenforPOST /render. - Because
EventSourcecannot send custom headers, havePOST /renderreturn a random per-job access token, then require that token for/render/:id/events,/render/:id/file, andDELETE /render/:id. - Add render-server settings in the client if the token must be user-configurable.
- Add
- Add basic abuse controls:
- Lower
express.jsonfrom200mbto a documented default, with an environment override. - Reject requests with a too-large
Content-Lengthbefore JSON parsing. - Add per-IP request rate limits for
/render. - Add
MAX_ACTIVE_JOBSandMAX_QUEUED_JOBS; return429or503when full. - Add a per-job timeout that aborts Puppeteer and
ffmpeg.
- Lower
- Improve job cleanup:
- Delete failed and aborted job directories.
- Remove completed job metadata after download TTL expires.
- Ensure
ffmpegis 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.jsserver/Dockerfiledocker-compose.ymlREADME.md
Tasks:
- Run the container as a non-root user:
- Create an app user/group in
server/Dockerfile. - Ensure
/appand temporary render directories are writable by that user. - Set
USERbeforeCMD.
- Create an app user/group in
- Remove unconditional sandbox-disabling Chromium flags:
- Try launching Chromium without
--no-sandboxand--disable-setuid-sandbox. - Keep an explicit
CHROME_NO_SANDBOX=1escape hatch only if needed for a known deployment environment. - Log a loud warning when the escape hatch is used.
- Try launching Chromium without
- 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.
- 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
ffmpegprocess.
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.tssrc/lib/ruwFile.tssrc/lib/zipUtil.tsserver/server.js- possibly
server/validation.jsor a shared package if server reuse becomes necessary
Tasks:
- Add centralized limits:
- Keep numeric bounds in one module.
- Use browser-safe limits for imports and stricter server-safe limits for rendering if needed.
- 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
ProjectFileErrormessages rather than raw parser errors.
- Validate
- Add render-request validation:
- Validate
sceneandoptionsbefore creating a job. - Clamp or reject
fps,duration,crf,preset, canvas size, and total frame count. - Compute
totalFramesonly after validation.
- Validate
- 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
DataVieworsubarray. - Reject path traversal and absolute paths even though extraction is in-memory.
- Preserve STORE-only behavior.
- Add import tests:
- Valid legacy
.jsonand.ruwstill 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.
- Valid legacy
Acceptance:
- Project import returns a safe internal
Projectshape. .ruwparsing 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.jsonserver/Dockerfile- root CI config when one exists
README.md
Tasks:
- Generate and commit
server/package-lock.json. - Replace
npm install --omit=devwithnpm ci --omit=devinserver/Dockerfile. - Add server audit/documentation:
npm audit --prefix server --omit=dev- Add the command to local release checks or CI.
- Rebuild the server image from a clean checkout to verify the lockfile path.
Acceptance:
npm audit --prefix server --omit=devcan 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.vuesrc/devtools/TestOverlay.vuesrc/devtools/tests.ts
Tasks:
- Replace the static
TestOverlayimport with a development-only dynamic import:- Use
defineAsyncComponentonly whenimport.meta.env.DEVis true. - Mount the overlay only in development.
- Use
- Guard menu/command paths that open the test runner:
- Keep Electron menu gating already present.
- Ensure renderer-side command handling ignores
test-runnerin production.
- Verify bundle output:
- Run
npm run build. - Confirm production chunks do not include
src/devtools/tests.tsor the test overlay.
- Run
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:
- Replace string-prefix URL checks with parsed
URLchecks. - Define allowed renderer origins:
- Development: exactly the configured Vite origin.
- Production: the packaged
file://app entry and same-directory app assets.
- 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 unexpectedhttp:.
- Prefer
- 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:
- Add an app-shell CSP meta tag.
- 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
- Verify the Electron renderer and browser app do not log CSP violations for normal workflows.
- 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
- Render server exposure and authentication.
- Render server job limits, body limits, and cleanup.
- Container non-root user and Chromium sandbox work.
- Shared limits plus render-request validation.
- Project and
.ruwimport validation. - Server lockfile and Docker
npm ci. - Production devtools gating.
- Electron URL policy.
- CSP.
Final Verification Checklist
npm run buildnpm audit --omit=devnpm 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
.jsonand.ruwfiles 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.