fix(913): scripted playout callbacks authenticate with a build-scoped token #939
Open
timothy
wants to merge 1 commits from
913-scripted-playout-401 into main
pull from: 913-scripted-playout-401
merge into: :main
:main
:renovate/meziantou.analyzer-3.x
:renovate/nuget-sixlabors.imagesharp-vulnerability
:913-scripted-playout-401
:929-vuln-pkcs-imagesharp-v4
:renovate/dotnet-monorepo
:release/v26.15.0-notes
:renovate/lucene.net
:renovate/cliwrap-3.x
:issue-806-guard-populations
:scratch/767b-poisoned
:scratch/767b-control
:release/v26.14.0-notes
:release/v26.14.0
:renovate/sqlitepclraw.bundle_e_sqlite3-3.x
:docs/510-skill-logo-bug-policy
:fix/510-watermark-resolution-policy
:fix/629-verdict-classifier-falseopens
:fix/609-decisions-edit-token-scope
:issue-135-clear-to-none
:release/v26.12.0-notes
:fix/409b-lastscan-api-parity
:fix/401-updatechannel-mirror-422
:fix/327-playlist-rename-validation
:fix/410-scancancel-log-level
:fix/409-447-librariesscreen-neverscanned
:fix/338-zap-exit-code
:fix/367-plex-budget-message
:fix/310-debom-legacy-cs
:ci/604-lane-rebalance
:feat/388-design-mirror
:feat/247-test-ownership
:feat/247-primary-action
:feat/357-player-owned-playback
:feat/357-jellyfin-plugin-poc
:fix/289-mcp-hardening
:issue58-mcp
:feat/244-channels-extract
:ci/auto-bump-prod-compose
:feat/multi-rerun-collections-api
:feat/collections-api
:feat/quick-wins
:feat/185-docs-part2
:feat/140-collections-screen
:feat/146-channel-edit
:feat/147-classic-ui-link
:issue22-renovate-dashboard
:feat/91-cutover
:feat/63-composite-create
:feat/65-library-browse
:feat/85-epg
:feat/86-schedule-editor
:feat/109-dashboard-data
:feat/99-session-tracking
:fix/dockerfile-node-tag
:feat/59-spa-foundation
:docs/59-ui-redesign-brief
:feat/102-json-guide
:feat/111-schedule-durations
:feat/104-artwork-upload
:feat/103-media-sources-api
:feat/playouts-read-api
:feat/108-health-api
:feat/105-picker-list-endpoints
:issue-97-channel-state-api
:issue42-jellyfin-musicvideos
:issue46-rest-api-error-contract
:dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
:qsv-improvements
:hdr-vulkan-cuda-test
No Reviewers
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#939
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
fixes #913
Root cause
ScriptedScheduleController(/api/v1/scripted/playout/build/{buildId}) sits under the globalApiAuthorizationFilter(mutating verbs always need a key; reads too sinceApi:RequireKeyForReadsdefaults true).ScriptedPlayoutBuilderlaunched the user's program with host, build id and mode only, andentrypoint.pybuilt its client with no credential, so every callback was a 401 and a scripted build produced nothing. #563 characterises scripted playout in-process with no filter, which is why nothing caught it.Fix: a build-scoped credential, not
[SkipApiAuthorization]StartSessionmints a 256-bit CSPRNG token per build (independent of the build id).ScriptedPlayoutBuilderpasses it inETV_SCRIPTED_BUILD_TOKEN(never argv: the arguments are logged and visible in the process table; the positional contract is unchanged). The script sends it asX-Scripted-Build-Token.[AcceptsScriptedBuildToken]; the filter accepts the token on that controller only, constant-time compared, valid only for the build named by the routebuildIdand only untilEndSession. Machine key and session still work.[SkipApiAuthorization]on the whole controller. It would widen a mutating surface to any local process (and anything that can reach localhost:UiPort), for all builds, forever; a scoped credential costs one header and keeps the surface fail-closed. Decision record:api.scripted-build-token.entrypoint.pyreads the env var and sets the header;docs/api-conventions.mdsection 9 anddocs/testing.mdupdated;testing.scripted-engine-in-process-net401 clause updated.Tests
ScriptedScheduleAuthorizationTestsdrives the real filter over the endpoint metadata MVC builds from the productionStartupfor every action: no credential 401, running build's token accepted, other build / ended build / wrong / empty token and token on an unmarked endpoint 401, machine key still works.ScriptedBuildTokenTestscovers minting, validation and the launch environment.Measured (2026-10-30, rebased on origin/main)
scripts/e2e-local.sh, port 8510, fresh config, Scripted playout whose program is a curl probe): the program received 3 args plus the env token; callback with no credential 401, with the build token 200, the same token against another build id 401; machine key on an unknown build 404, bogus token 401; logged launch arguments contain no token. The pre-fix state (no credential sent) is the "no credential 401" row.Review history
<<REVIEW HISTORY
round 1: 3 lens(es); 0 blocking, 0 should-fix, 12 nit; clean — loop ended
REVIEW HISTORY>>
No fix commit exists because round one was clean.
Cross-family review: codex could not run in round 1 (no VERDICT line); substituted a cold same-family review-only agent per process.independent-review-rubric — retry cross-family next window
Deliberately left
The 12 round-one nits are not blocking and are not separately tracked; the
Cli.Wraplaunch hop itself stays outside the automated suite (testing.scripted-engine-in-process-net), held byScriptedBuildTokenTestsonCreateCommandand the live run above.🤖 Generated with Claude Code
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.