Same-repo pull_request runs get REGISTRY_PASSWORD from head-supplied YAML — the route #853 found under the dispatch class
#885
Open
opened 2026-08-30 01:40:56 +02:00 by timothy
·
1 comment
No Branch/Tag Specified
main
renovate/meziantou.analyzer-3.x
release/v26.15.0-notes
fix/830-add-items-error-surface
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
renovate/dotnet-monorepo
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
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
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
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#885
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.
Split out of #853, whose probe found this while establishing that
workflow_dispatchis not the narrowest route to the registry credential. Recorded inci.workflow-dispatch-ref-unrestricted.The route
.gitea/workflows/docker-build.ymltriggers onpull_request:. Gitea resolves that from the PR head, so the run executes attacker-authored YAML. That is the general form of the problem, and it is bigger than the jobs listed below: head-supplied YAML can name any secret in the repo store, not only the ones the committed workflows happen to reference.ci.gate-trigger-base-resolvedalready states this — "any PR-added workflow can referenceRENOVATE_TOKEN, awrite:repositorybot PAT in the same store".The invariant is "every job on the PR route that names
secrets.REGISTRY_PASSWORD", not a list — and NOT "everycontainer:job", which names only five of six (see the correction comment below). Today that is six jobs indocker-build.ymlholdingREGISTRY_PASSWORDand running onpull_request:toolchain-preflighttestmigrationsfunctional-e2epull_request || ref == mainapi-docsevent_name == 'pull_request'formatevent_name == 'pull_request'(
build, line 820, is the onlyREGISTRY_PASSWORDjob excluded —if: github.event_name != 'pull_request'.)testandmigrationsare both branch-protection required contexts per.gitea/required-status-contexts.json.A fix scoped to today's six names will miss the seventh. Derive the population; do not restate it —
testing.guard-derives-population-from-source, and thean-issues-file-list-is-not-the-populationshape.So "push a branch, open a PR" reaches the credential with no act outside the ordinary contribution flow, where a dispatch costs one. The per-step
if:ref gates on the publish steps (lines 906/934) are not a mitigation — they live in the same head-supplied file and an attacker edits them out.This is why #853 was accepted rather than fixed: restricting dispatch would have closed the more deliberate of two routes and left the cheaper one open. That is the finding, not a deferral of it.
Actor set (measured 2026-08-30)
Exactly two write-capable accounts:
timothy(owner,is_admin: true) andrenovate(permission: write,is_admin: false). Against the admin no self-administered control is a boundary. Against the bot it is a real gap:review-verdict/h10and the Renovate exemption rule (#698, #742, #845) bound what Renovate can get merged; nothing bounds what it can run.Two separable pieces of work
1. The
pull_request:credential exposure (the hard one). Gitea 1.27.1 has no fork/first-run approval gate and no per-environment secret scoping — probed in #853 across the REST API (308 paths), the loadedapp.ini, and thegiteaCLI. So the options are design-side, and each has a real cost:ci.actions-credential-scopingalready establishes thatread:repository/write:packageare separable scopes, so a pull-only token looks feasible. Cost this first. Note it bounds the blast radius rather than closing the route: head YAML can still name other secrets.2.
tag_protectionsis empty (the cheap one). Live state is[]. Gitea 1.27.1 does support tag protection (name_pattern+whitelist_usernames/whitelist_teams). Av*rule whitelisting the release operator closesdocker-build.yml's tag-push row against the bot at no operational cost, since only the operator cuts releases. Deliberately not applied in #853's PR: its failure mode is a broken release cut, so it needs its own verification that a legitimatev*push still succeeds. Note it buys nothing against the admin actor.Do these as two changes, not one — (2) is configuration with a release-cut blast radius, (1) is a credential redesign.
Done-when
secrets.REGISTRY_PASSWORDand runs on the PR route", so a new job cannot silently join it unprotectedv*tag protection is applied and a real release-cut tag push is verified to still succeed, or the rule is rejected with its reasonci.workflow-dispatch-ref-unrestrictedandci.actions-credential-scopingare updated to match whatever is decidedCorrection to this issue's body — the invariant I wrote in it is wrong, and wrong in exactly the way it warns against.
The body said the population is "every
container:job on the PR route". It is not.toolchain-preflight(docker-build.yml:141) is deliberately container-free —runs-on: small, nocontainer:key, and the credential enters at:154viaETV_REGISTRY_AUTHrather than a registry login.docker-build.ymlhas exactly fivecontainer:blocks, so that predicate names five of the six PR-route jobs and would go stale on day one.The correct predicate is every job on the PR route that names
secrets.REGISTRY_PASSWORD. Re-derived againstmainat94a3d1349(after #884, which toucheddocker-build.yml):Six on the PR route; five of them
container:. The body's table remains correct — only the stated invariant was wrong.This matters for the Done-when box that asks the population to be derived rather than listed: derived by the wrong predicate is not better than a list, it is a list with a false claim of completeness attached. Found by cold review of #853's PR.