Summary
Defense-in-depth input hardening for the game-streamer spec-server (:1350). Three unrelated-but-adjacent gaps on the same server: request bodies are buffered with no size or time limit, job_id is used verbatim in filesystem paths, and output_fps is accepted with no range clamp. This is a companion to the spec-server authentication issue (#531); even once the endpoint is authenticated, these values should be validated.
Location
game-streamer:
src/spectator/util/http.mjs:17-29 : readJsonBody accumulates every chunk into an in-memory array and only concatenates on end, with no byte cap and no idle/overall timeout (Node imposes no default body-size limit). Every POST route goes through it. (JSON.parse itself is correctly wrapped in try/catch.)
src/spectator/routes/render-clip.mjs:10 : jobId = String(body.job_id ?? "") with no format check, then passed as CLIP_RENDER_JOB_ID env into lib/inline-clip-render.sh, which interpolates it into output paths (e.g. ${CLIP_OUT_DIR}/${CLIP_RENDER_JOB_ID}.mp4). A traversal value escapes the clips directory on write/rm.
src/spectator/routes/render-clip.mjs:14 : outputFps = Number.parseInt(body.output_fps, 10) || 60 accepts negatives and absurdly large values (only 0/NaN fall back to 60); forwarded as CLIP_OUTPUT_FPS into Remotion (durationInFrames = duration * fps) and ffmpeg.
Impact
- Body size: a multi-gigabyte or slow-loris body to any POST route buffers in RAM until the pod OOM-kills; because the pod is host-networked, that takes streaming down for the whole node. Triggerable even by a well-behaved-but-buggy caller.
job_id traversal: a value like ../../../opt/game-streamer/resources/video/outro_1920x1080_60 makes rm -f/ffmpeg target files outside the intended directory. Job ids are normally server-generated UUIDs, so this needs a buggy/compromised api, hence P3, but the route offers no defense.
output_fps: a huge value makes Remotion compute an astronomical frame count and pin a render slot/GPU indefinitely; a negative value makes ffmpeg abort the job.
Suggested Fix
readJsonBody: reject with 413 once accumulated length exceeds a small cap (for example 1 MB) and enforce a request timeout, destroying the socket when exceeded.
job_id: reject anything not matching /^[A-Za-z0-9_-]{1,64}$/ before spawning the render.
output_fps: clamp to a sane range, e.g. Math.min(120, Math.max(1, parsed)) defaulting to 60.
Found by the 2026-07 multi-agent audit; verified against current code. Companion to #531 (spec-server authentication).
Summary
Defense-in-depth input hardening for the game-streamer spec-server (
:1350). Three unrelated-but-adjacent gaps on the same server: request bodies are buffered with no size or time limit,job_idis used verbatim in filesystem paths, andoutput_fpsis accepted with no range clamp. This is a companion to the spec-server authentication issue (#531); even once the endpoint is authenticated, these values should be validated.Location
game-streamer:src/spectator/util/http.mjs:17-29:readJsonBodyaccumulates every chunk into an in-memory array and only concatenates onend, with no byte cap and no idle/overall timeout (Node imposes no default body-size limit). Every POST route goes through it. (JSON.parseitself is correctly wrapped in try/catch.)src/spectator/routes/render-clip.mjs:10:jobId = String(body.job_id ?? "")with no format check, then passed asCLIP_RENDER_JOB_IDenv intolib/inline-clip-render.sh, which interpolates it into output paths (e.g.${CLIP_OUT_DIR}/${CLIP_RENDER_JOB_ID}.mp4). A traversal value escapes the clips directory on write/rm.src/spectator/routes/render-clip.mjs:14:outputFps = Number.parseInt(body.output_fps, 10) || 60accepts negatives and absurdly large values (only0/NaNfall back to 60); forwarded asCLIP_OUTPUT_FPSinto Remotion (durationInFrames = duration * fps) and ffmpeg.Impact
job_idtraversal: a value like../../../opt/game-streamer/resources/video/outro_1920x1080_60makesrm -f/ffmpeg target files outside the intended directory. Job ids are normally server-generated UUIDs, so this needs a buggy/compromised api, hence P3, but the route offers no defense.output_fps: a huge value makes Remotion compute an astronomical frame count and pin a render slot/GPU indefinitely; a negative value makes ffmpeg abort the job.Suggested Fix
readJsonBody: reject with 413 once accumulated length exceeds a small cap (for example 1 MB) and enforce a request timeout, destroying the socket when exceeded.job_id: reject anything not matching/^[A-Za-z0-9_-]{1,64}$/before spawning the render.output_fps: clamp to a sane range, e.g.Math.min(120, Math.max(1, parsed))defaulting to 60.Found by the 2026-07 multi-agent audit; verified against current code. Companion to #531 (spec-server authentication).