Skip to content

[Game Streamer] Spec-server input hardening (body size, job_id path, output_fps clamp) #543

Description

@Flegma

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2-mediumCode quality & robustnessaudit-2026-07Findings from the 2026-07 code audit / review passreliabilityReliability or availability concernsecuritySecurity vulnerability or hardeningservice:game-streamerHighlight/clip render pipeline (game-streamer)

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions