Grouped game-streamer render-pipeline robustness defects: leaked scratch files that fill the batch pod disk, and a JSON escaping bug in the title patch. Also noted (plausible, lower confidence): PlayerChip renders avatarUrl with no scheme allowlist in the headless-Chromium render pod, an SSRF/local-file concern if a non-Steam avatar URL ever reaches it.
Player-chip ProRes .mov leaked on every successful clip render, filling batch pod scratch disk
Location: src/lib/inline-clip-render.sh:1567 (game-streamer)
What: CHIP_MOV is created at ${CLIP_OUT_DIR}/${JOB_ID}-chip.mov (line 626) as a ProRes 4444 intermediate (tens of MB for a ~3.5s card at output dims). Its ONLY deletion sites are the EXIT trap on_exit (line 226) and wait_for_chip_render's failure branch (line 692). On the success path the EXIT trap is disarmed at line 1478 (trap - EXIT) before the normal exit, and wait_for_chip_render only removes the file when the render FAILED. The success cleanup here removes CLIP_OUT_FILE and earlier removes SEG_DIR (line 1467), but never removes CHIP_MOV. batch-highlights runs many jobs in one pod all sharing CLIP_OUT_DIR (=/tmp/game-streamer/clips), so each successfully rendered highlight leaves its chip .mov behind. Branding (hence chip creation) is on by default (BRANDING_ENABLED=1, line 581).
Impact: A batch-highlights pod renders 30 branded highlight jobs. Each success leaves a ~30-70MB ${JOB_ID}-chip.mov in the shared /tmp/game-streamer/clips. After ~1-2GB of accumulation the pod's scratch (emptyDir on node disk) fills; there is no disk-full handling, so subsequent gst captures / ffmpeg encodes fail and later jobs error out.
Suggested fix: In the success path (near the CLIP_OUT_FILE removal at line 1567, or at the end of wait_for_chip_render after the chip is consumed), add [ -n "${CHIP_MOV:-}" ] && rm -f "$CHIP_MOV" so the intermediate is dropped on success just as it is on error.
Verifier evidence
src/lib/inline-clip-render.sh:626 CHIP_MOV="${CLIP_OUT_DIR:-/tmp/game-streamer/clips}/${CLIP_RENDER_JOB_ID}-chip.mov"; :581 BRANDING_ENABLED="${CLIP_BAKE_BRANDING:-1}"; :594 gate creates chip when branding on and CLIP_DISABLE_CHIP!=1; :226 (EXIT trap) if [ -n "${CHIP_MOV:-}" ]; then rm -f "$CHIP_MOV"; fi; :692-693 removal only inside if ! wait "$CHIP_RENDER_PID"; then ... rm -f "$CHIP_MOV" (failure branch); :1478 trap - EXIT disarms trap on success; :1567 rm -f "$CLIP_OUT_FILE" with no corresponding CHIP_MOV removal before script end at :1568; batch-highlights.sh:88-104 subshell export CLIP_RENDER_JOB_ID=... ; bash "$LIB_DIR/inline-clip-render.sh" sharing default CLIP_OUT_DIR, sets neither CLIP_DISABLE_CHIP nor CLIP_BAKE_BRANDING; loop only rm -f "$marker" (:110,122,137).
Failed clip render leaves full segment directory and output mp4 on disk
Location: src/lib/inline-clip-render.sh:226 (game-streamer)
What: SEG_DIR (${CLIP_OUT_DIR}/${JOB_ID}.segs, holding one raw-capture mp4 per segment) is only removed at line 1467 on the success path, and CLIP_OUT_FILE only at line 1567. The on_exit EXIT trap (lines 205-238) that runs on die_failed / SIGTERM / set-u trip cleans chip mov + polish/chip logs but never removes SEG_DIR or CLIP_OUT_FILE. A job that captures several segments and then dies (e.g. ffmpeg concat failure at line 1443, or upload failure at line 1557) leaves its whole seg dir on the shared batch scratch.
Impact: In a batch pod, a job captures 5 segments (each tens of MB) and then die_failed fires at the concat filter-graph step. The ${JOB_ID}.segs directory with all raw segment mp4s survives. Repeated across several failing jobs in a large batch this accumulates on the shared /tmp scratch alongside the chip-mov leak.
Suggested fix: In on_exit, on non-zero rc also rm -rf "${SEG_DIR:-}" and rm -f "${CLIP_OUT_FILE:-}" (guarding for unset since they are defined later in the script).
Verifier evidence
src/lib/inline-clip-render.sh:222-226 on_exit removes only logs + CHIP_MOV: [ -n "${POLISH_BG_LOG:-}" ] && rm -f "$POLISH_BG_LOG" / [ -n "${CHIP_RENDER_LOG:-}" ] && rm -f "$CHIP_RENDER_LOG" / if [ -n "${CHIP_MOV:-}" ]; then rm -f "$CHIP_MOV"; fi (no SEG_DIR / CLIP_OUT_FILE). die_failed at 165-172 is say...; api_status "status=error"...; CLIP_REACHED_TERMINAL=1; exit 1 with no rm. Line 747 SEG_DIR="${CLIP_OUT_DIR}/${CLIP_RENDER_JOB_ID}.segs"; line 866 SEG_FILE="${SEG_DIR}/seg-$(printf '%03d' "$SEG_IDX").mp4". Line 1443 die_failed "ffmpeg concat (filter-graph) failed" and 1463 die_failed "ffmpeg concat failed" both precede line 1467 rm -rf "$SEG_DIR" (only SEG_DIR removal in the file). Line 1557 die_failed "clip upload failed (curl rc=${UPLOAD_RC})" precedes line 1567 rm -f "$CLIP_OUT_FILE" (only success-path removal of the output).
Title-patch JSON only escapes double-quotes, so a backslash in a player name produces invalid JSON
Location: src/lib/batch-highlights.sh:36 (game-streamer)
What: patch_title_from_gsi builds the request body with --data "$(printf '{"title": "%s"}' "${new_title//\"/\\\"}")". The parameter expansion escapes only " (to \"); it does not escape backslashes or control characters. new_title derives from the GSI in-game player name (name-for-steamid), which is attacker-influenced (a player's Steam/in-game name). A name containing a backslash yields a body like {"title": "foo"} where the trailing backslash escapes the closing quote, producing invalid JSON.
Impact: A player sets their in-game name to end with a backslash (e.g. clutch\). When their highlight is batch-rendered, the title-patch POST to /clip-renders/:id/title carries malformed JSON, the api returns 4xx, and the title patch silently fails (logged as WARN title patch failed); the clip keeps the placeholder "Player NNNN" title.
Suggested fix: Encode the title through the existing node clip-helpers JSON encoder (as status-body / patch-player-name already do) instead of hand-rolling printf escaping, so backslashes and control chars are handled.
Verifier evidence
src/lib/batch-highlights.sh:36 --data "$(printf '{"title": "%s"}' "${new_title//\"/\\\"}")" \ (only "->\", backslashes not escaped)
src/lib/batch-highlights.sh:22-29 new_title <- patch-player-name(resolved <- name-for-steamid target_sid)
src/lib/clip-helpers.mjs name-for-steamid: const m = slots.find((s) => s?.steam_id === sid); process.stdout.write(typeof m?.name === "string" ? m.name : ""); (name is raw GSI player name)
src/lib/clip-helpers.mjs patch-player-name: title.replace(/^Player [A-Za-z0-9_-]+/, () => newName) (splices raw name into title)
Contrast src/lib/batch-highlights.sh:45 body=$(node "$CLIP_HELPERS" status-body ...) and clip-helpers status-body/job-fields using JSON.stringify (proper encoding elsewhere)
src/lib/batch-highlights.sh:33-39 curl uses --fail and falls back to say " WARN title patch failed for $job_id"
Remotion PlayerChip renders avatarUrl with no scheme allowlist (SSRF/local-file in render pod)
Location: motion/src/PlayerChip.tsx:138 (game-streamer)
What: playerChipSchema declares avatarUrl as z.string().nullable() with no URL/scheme validation (line 42), and the component renders it directly as
in headless Chromium. The value flows from the render job's spec.target_avatar_url (clip-helpers.mjs job-fields F[8] -> batch-highlights.sh CLIP_DISPLAY_AVATAR -> inline-clip-render.sh CHIP_AVATAR -> --props avatarUrl), which is player-derived data forwarded by the api rather than a validated constant.
Impact: If a job spec's target_avatar_url is ever attacker-influenced (custom/player-supplied avatar URL), the render pod's Chromium fetches an arbitrary scheme/host chosen by the value, e.g. http://169.254.169.254/... (cloud metadata) or another internal service, and bakes the response into the published clip, and file:// / internal hosts are not blocked by any allowlist.
Suggested fix: Constrain avatarUrl in playerChipSchema to an https/data URL allowlist (z.string().url().refine(scheme in {https:,data:})) and null out anything else before render.
Verifier evidence
motion/src/PlayerChip.tsx:42 avatarUrl: z.string().nullable().default(null),\nmotion/src/PlayerChip.tsx:137-138 <Img / src={avatarUrl}\nsrc/lib/clip-helpers.mjs:242 const S = (v) => (typeof v === "string" ? v.replaceAll("\\u0000", "") : "");\nsrc/lib/clip-helpers.mjs:259 S(d?.spec?.target_avatar_url),\nsrc/lib/batch-highlights.sh:97 export CLIP_DISPLAY_AVATAR="$target_avatar"\nsrc/lib/inline-clip-render.sh:603 CHIP_AVATAR="${CLIP_DISPLAY_AVATAR:-}"\nsrc/lib/inline-clip-render.sh:640 avatarUrl: process.env.CHIP_AVATAR || null,\nNo allowlist/scheme guard: grep for steamstatic|scheme|allowlist|.url( across src/ and motion/src/ returned no matches.
Found by the 2026-07 multi-agent code audit; adversarially verified (CONFIRMED, P2/P3). One of a batch of findings from that pass.
Grouped game-streamer render-pipeline robustness defects: leaked scratch files that fill the batch pod disk, and a JSON escaping bug in the title patch. Also noted (plausible, lower confidence): PlayerChip renders avatarUrl with no scheme allowlist in the headless-Chromium render pod, an SSRF/local-file concern if a non-Steam avatar URL ever reaches it.
Player-chip ProRes .mov leaked on every successful clip render, filling batch pod scratch disk
Location:
src/lib/inline-clip-render.sh:1567(game-streamer)What: CHIP_MOV is created at ${CLIP_OUT_DIR}/${JOB_ID}-chip.mov (line 626) as a ProRes 4444 intermediate (tens of MB for a ~3.5s card at output dims). Its ONLY deletion sites are the EXIT trap on_exit (line 226) and wait_for_chip_render's failure branch (line 692). On the success path the EXIT trap is disarmed at line 1478 (
trap - EXIT) before the normal exit, and wait_for_chip_render only removes the file when the render FAILED. The success cleanup here removes CLIP_OUT_FILE and earlier removes SEG_DIR (line 1467), but never removes CHIP_MOV. batch-highlights runs many jobs in one pod all sharing CLIP_OUT_DIR (=/tmp/game-streamer/clips), so each successfully rendered highlight leaves its chip .mov behind. Branding (hence chip creation) is on by default (BRANDING_ENABLED=1, line 581).Impact: A batch-highlights pod renders 30 branded highlight jobs. Each success leaves a ~30-70MB ${JOB_ID}-chip.mov in the shared /tmp/game-streamer/clips. After ~1-2GB of accumulation the pod's scratch (emptyDir on node disk) fills; there is no disk-full handling, so subsequent gst captures / ffmpeg encodes fail and later jobs error out.
Suggested fix: In the success path (near the CLIP_OUT_FILE removal at line 1567, or at the end of wait_for_chip_render after the chip is consumed), add
[ -n "${CHIP_MOV:-}" ] && rm -f "$CHIP_MOV"so the intermediate is dropped on success just as it is on error.Verifier evidence
src/lib/inline-clip-render.sh:626
CHIP_MOV="${CLIP_OUT_DIR:-/tmp/game-streamer/clips}/${CLIP_RENDER_JOB_ID}-chip.mov"; :581BRANDING_ENABLED="${CLIP_BAKE_BRANDING:-1}"; :594 gate creates chip when branding on and CLIP_DISABLE_CHIP!=1; :226 (EXIT trap)if [ -n "${CHIP_MOV:-}" ]; then rm -f "$CHIP_MOV"; fi; :692-693 removal only insideif ! wait "$CHIP_RENDER_PID"; then ... rm -f "$CHIP_MOV"(failure branch); :1478trap - EXITdisarms trap on success; :1567rm -f "$CLIP_OUT_FILE"with no corresponding CHIP_MOV removal before script end at :1568; batch-highlights.sh:88-104 subshellexport CLIP_RENDER_JOB_ID=... ; bash "$LIB_DIR/inline-clip-render.sh"sharing default CLIP_OUT_DIR, sets neither CLIP_DISABLE_CHIP nor CLIP_BAKE_BRANDING; loop onlyrm -f "$marker"(:110,122,137).Failed clip render leaves full segment directory and output mp4 on disk
Location:
src/lib/inline-clip-render.sh:226(game-streamer)What: SEG_DIR (${CLIP_OUT_DIR}/${JOB_ID}.segs, holding one raw-capture mp4 per segment) is only removed at line 1467 on the success path, and CLIP_OUT_FILE only at line 1567. The on_exit EXIT trap (lines 205-238) that runs on die_failed / SIGTERM / set-u trip cleans chip mov + polish/chip logs but never removes SEG_DIR or CLIP_OUT_FILE. A job that captures several segments and then dies (e.g. ffmpeg concat failure at line 1443, or upload failure at line 1557) leaves its whole seg dir on the shared batch scratch.
Impact: In a batch pod, a job captures 5 segments (each tens of MB) and then die_failed fires at the concat filter-graph step. The ${JOB_ID}.segs directory with all raw segment mp4s survives. Repeated across several failing jobs in a large batch this accumulates on the shared /tmp scratch alongside the chip-mov leak.
Suggested fix: In on_exit, on non-zero rc also
rm -rf "${SEG_DIR:-}"andrm -f "${CLIP_OUT_FILE:-}"(guarding for unset since they are defined later in the script).Verifier evidence
src/lib/inline-clip-render.sh:222-226 on_exit removes only logs + CHIP_MOV:
[ -n "${POLISH_BG_LOG:-}" ] && rm -f "$POLISH_BG_LOG"/[ -n "${CHIP_RENDER_LOG:-}" ] && rm -f "$CHIP_RENDER_LOG"/if [ -n "${CHIP_MOV:-}" ]; then rm -f "$CHIP_MOV"; fi(no SEG_DIR / CLIP_OUT_FILE). die_failed at 165-172 issay...; api_status "status=error"...; CLIP_REACHED_TERMINAL=1; exit 1with no rm. Line 747SEG_DIR="${CLIP_OUT_DIR}/${CLIP_RENDER_JOB_ID}.segs"; line 866SEG_FILE="${SEG_DIR}/seg-$(printf '%03d' "$SEG_IDX").mp4". Line 1443die_failed "ffmpeg concat (filter-graph) failed"and 1463die_failed "ffmpeg concat failed"both precede line 1467rm -rf "$SEG_DIR"(only SEG_DIR removal in the file). Line 1557die_failed "clip upload failed (curl rc=${UPLOAD_RC})"precedes line 1567rm -f "$CLIP_OUT_FILE"(only success-path removal of the output).Title-patch JSON only escapes double-quotes, so a backslash in a player name produces invalid JSON
Location:
src/lib/batch-highlights.sh:36(game-streamer)What: patch_title_from_gsi builds the request body with
--data "$(printf '{"title": "%s"}' "${new_title//\"/\\\"}")". The parameter expansion escapes only"(to\"); it does not escape backslashes or control characters. new_title derives from the GSI in-game player name (name-for-steamid), which is attacker-influenced (a player's Steam/in-game name). A name containing a backslash yields a body like {"title": "foo"} where the trailing backslash escapes the closing quote, producing invalid JSON.Impact: A player sets their in-game name to end with a backslash (e.g.
clutch\). When their highlight is batch-rendered, the title-patch POST to /clip-renders/:id/title carries malformed JSON, the api returns 4xx, and the title patch silently fails (logged asWARN title patch failed); the clip keeps the placeholder "Player NNNN" title.Suggested fix: Encode the title through the existing node clip-helpers JSON encoder (as status-body / patch-player-name already do) instead of hand-rolling printf escaping, so backslashes and control chars are handled.
Verifier evidence
src/lib/batch-highlights.sh:36
--data "$(printf '{"title": "%s"}' "${new_title//\"/\\\"}")" \(only"->\", backslashes not escaped)src/lib/batch-highlights.sh:22-29 new_title <- patch-player-name(resolved <- name-for-steamid target_sid)
src/lib/clip-helpers.mjs name-for-steamid:
const m = slots.find((s) => s?.steam_id === sid); process.stdout.write(typeof m?.name === "string" ? m.name : "");(name is raw GSI player name)src/lib/clip-helpers.mjs patch-player-name:
title.replace(/^Player [A-Za-z0-9_-]+/, () => newName)(splices raw name into title)Contrast src/lib/batch-highlights.sh:45
body=$(node "$CLIP_HELPERS" status-body ...)and clip-helpers status-body/job-fields using JSON.stringify (proper encoding elsewhere)src/lib/batch-highlights.sh:33-39 curl uses --fail and falls back to
say " WARN title patch failed for $job_id"Remotion PlayerChip renders avatarUrl with no scheme allowlist (SSRF/local-file in render pod)
Location:
motion/src/PlayerChip.tsx:138(game-streamer)What: playerChipSchema declares avatarUrl as z.string().nullable() with no URL/scheme validation (line 42), and the component renders it directly as
in headless Chromium. The value flows from the render job's spec.target_avatar_url (clip-helpers.mjs job-fields F[8] -> batch-highlights.sh CLIP_DISPLAY_AVATAR -> inline-clip-render.sh CHIP_AVATAR -> --props avatarUrl), which is player-derived data forwarded by the api rather than a validated constant.
Impact: If a job spec's target_avatar_url is ever attacker-influenced (custom/player-supplied avatar URL), the render pod's Chromium fetches an arbitrary scheme/host chosen by the value, e.g. http://169.254.169.254/... (cloud metadata) or another internal service, and bakes the response into the published clip, and file:// / internal hosts are not blocked by any allowlist.
Suggested fix: Constrain avatarUrl in playerChipSchema to an https/data URL allowlist (z.string().url().refine(scheme in {https:,data:})) and null out anything else before render.
Verifier evidence
motion/src/PlayerChip.tsx:42
avatarUrl: z.string().nullable().default(null),\nmotion/src/PlayerChip.tsx:137-138<Img/src={avatarUrl}\nsrc/lib/clip-helpers.mjs:242const S = (v) => (typeof v === "string" ? v.replaceAll("\\u0000", "") : "");\nsrc/lib/clip-helpers.mjs:259S(d?.spec?.target_avatar_url),\nsrc/lib/batch-highlights.sh:97export CLIP_DISPLAY_AVATAR="$target_avatar"\nsrc/lib/inline-clip-render.sh:603CHIP_AVATAR="${CLIP_DISPLAY_AVATAR:-}"\nsrc/lib/inline-clip-render.sh:640avatarUrl: process.env.CHIP_AVATAR || null,\nNo allowlist/scheme guard: grep for steamstatic|scheme|allowlist|.url( across src/ and motion/src/ returned no matches.Found by the 2026-07 multi-agent code audit; adversarially verified (CONFIRMED, P2/P3). One of a batch of findings from that pass.