mirror of
https://github.com/astral-sh/setup-uv.git
synced 2026-06-22 20:42:36 +00:00
simplify
This commit is contained in:
+51
-211
@@ -2,238 +2,78 @@
|
||||
|
||||
## Overview
|
||||
|
||||
`setup-uv` is a TypeScript GitHub Action that installs `uv`, exposes `uv` and `uvx` to later steps, optionally creates a virtual environment, restores and saves the uv cache by default on GitHub-hosted runners, and can optionally cache uv-managed Python installations. It is not a network service and has no application users, sessions, tenants, database, or authorization layer. Its security boundary is supply-chain and CI execution: repository-maintained JavaScript downloads or reuses native executables inside a job that may hold filesystem, network, cache, repository-token, secret, OIDC, or release authority.
|
||||
`setup-uv` is a GitHub Action that installs or reuses `uv`, changes later-step paths and environment, may discover and execute a Python interpreter, may create or clear a virtual environment, and may restore or save caches. It runs with the workflow job's filesystem, network, token, secrets, OIDC, artifact, and release authority.
|
||||
|
||||
The shipped or privileged runtime is broader than `src/`:
|
||||
The consumer runtime is the selected ref's committed action metadata, bundles, and runner-interpreted companion files; source alone is not evidence of shipped behavior. Privileged automation that generates, updates, or publishes those artifacts is also in scope.
|
||||
|
||||
- `action.yml` selects Node 24, `dist/setup/index.cjs`, and the success-only post entry point `dist/save-cache/index.cjs`; `src/setup-uv.ts` and `src/save-cache.ts` are their authored sources.
|
||||
- The main bundle registers `.github/python.json`, which the GitHub runner interprets as a problem matcher.
|
||||
- `src/update-known-checksums.ts` and `dist/update-known-checksums/index.cjs` run in privileged repository automation, not consumer jobs.
|
||||
- `scripts/build-dist.mjs` produces the three committed CommonJS bundles.
|
||||
The assets are job credentials; integrity of installed executables, interpreter, environment, checkout, runner, artifacts, and caches; isolation between jobs sharing caches or persistent runners; integrity of published action refs; and workflow compute/storage availability.
|
||||
|
||||
This repository-scoped model was rechecked on 2026-06-17 against local checkout `88aa608651c03dd9c5c3132acb9f2da90b3e6e15` and upstream `main` at `ca5ddd015e07666cb1f1340ad6171f5e7afffb3e`. It models durable boundaries, not a guarantee that historical releases contain current mitigations. Every scan must inspect the selected ref's `action.yml` and committed bundles; source or live-`main` behavior is not evidence for an older bundle. GitHub rulesets, environments, token policy, cache policy, and runner allocation are external, mutable state and must be refreshed when material.
|
||||
|
||||
At runtime the action reads workflow inputs, runner state, and project files; resolves a version from explicit inputs/files, `uv.toml`, `pyproject.toml`, or `latest`; reuses a runner tool-cache entry or reads a default/custom manifest; downloads, conditionally hashes, extracts, and caches a platform artifact; exports paths, variables, outputs, and state; executes `uv`; normally runs `uv python find` and then the returned interpreter with `--version`; optionally runs `uv venv ... --clear`; and restores, prunes, and saves caches.
|
||||
|
||||
The protected assets are:
|
||||
|
||||
- the default or custom `github-token`, job secrets, OIDC, and later-step credentials;
|
||||
- integrity of `uv`, `uvx`, the discovered Python interpreter, virtual environment, `PATH`, checkout, runner filesystem, action command/state files, artifacts, and caches;
|
||||
- isolation between lower- and higher-authority jobs sharing GitHub caches or persistent runners;
|
||||
- the committed bundles, checksum table, releases, tags, draft targets, and documentation pins; and
|
||||
- workflow time, CPU, memory, disk, network, and cache quota.
|
||||
|
||||
Material failures include misbound executable selection, token disclosure, premature execution of lower-authority project/cache content, archive or cache writes outside an authorized root, cross-authority persistence, and compromise of automation that publishes trusted artifacts.
|
||||
Material failures are unauthorized executable selection, credential disclosure, premature execution of lower-authority content, filesystem escape or destructive path use, cross-authority cache/runner persistence, and unauthorized publication.
|
||||
|
||||
## Threat Model, Trust Boundaries, and Assumptions
|
||||
|
||||
### Actors and input authority
|
||||
### Authority and trust boundaries
|
||||
|
||||
| Actor | Authority and trust |
|
||||
| Actor or input | Trust decision |
|
||||
|---|---|
|
||||
| Maintainers and repository/configuration administrators | Trusted to control source, bundles, workflows, releases, rulesets, and environments. Their compromise has broad impact but is a trust-root compromise unless a lower-authority principal bypasses an expected gate. A PR or CI workflow does not by itself prove review or checks are enforced. |
|
||||
| Consumer workflow authors and runner operators | Select the action ref, trigger, runner, permissions, secrets, proxy, environment, and all `with:` inputs. `manifest-file`, `checksum`, `working-directory`, `venv-path`, `cache-local-path`, `tool-dir`, `tool-bin-dir`, and `cache-dependency-glob` are trusted operator choices unless derived from lower-authority event data. Choosing a custom manifest delegates metadata and executable authority; choosing a path authorizes normal operations on it and its intended referents. Current `main`, unlike the evidence checkout, also has `download-from-astral-mirror` and `quiet`; `quiet` changes information logging only. |
|
||||
| Project authors and pull-request contributors | May control `uv.toml`, `pyproject.toml`, requirements/version files, `.tool-versions`, `.python-version`, `.venv`, symlinks, cache dependency files, and other checkout state. This is ordinary developer input on a trusted branch, but attacker input when an unreviewed checkout runs with secrets, write/OIDC/artifact authority, or persistent state. |
|
||||
| Remote metadata and artifact authorities | The default design trusts HTTPS delivery from `raw.githubusercontent.com/astral-sh/versions`, official `astral-sh/uv` releases, and `releases.astral.sh`, plus GitHub, configured TLS roots, and an operator proxy. A selected custom manifest authorizes its URLs and hashes; a hash from that same authority detects corruption, not a malicious authority. |
|
||||
| Cache and runner-state producers/consumers | Same-job and same-principal tool, uv, and Python caches are trusted by default. Integrity flows lower producer -> higher consumer; confidentiality can flow higher producer -> lower consumer because fork PRs may restore eligible base-branch caches. Self-hosted administrators and same-account writers are trusted unless the deployment intentionally creates cross-principal sharing. |
|
||||
| GitHub Actions and dependencies | The runner protocol, cache service, hosted isolation, Node, npm registry at build time, `@actions/*`, and pinned actions are infrastructure trust roots. Native helpers resolved through `PATH` remain executable-selection boundaries. Repository-specific misuse of infrastructure is in scope; infrastructure compromise alone is not. |
|
||||
| GitHub-managed automation | Live Actions state includes dynamic Dependabot and Copilot workflows at `dynamic/dependabot/dependabot-updates`, `dynamic/copilot-swe-agent/copilot`, `dynamic/copilot-pull-request-reviewer/copilot-pull-request-reviewer`, and `dynamic/agents/copilot-pull-request-reviewer`, plus an unprotected `copilot` environment. These are external, drift-prone principals. Their presence proves no particular permission: obtain live trigger, actor, token, environment, ref, and write/secret authority. |
|
||||
| Maintainers, repository/configuration administrators, and GitHub infrastructure | Trusted roots for source, bundles, workflows, refs, rulesets, environments, runner protocol, hosted isolation, and cache service. A lower-authority path into these roots is in scope; their compromise alone is not a repository bug. |
|
||||
| Consumer workflow authors and runner operators | Control the action ref, trigger, runner, permissions, secrets, proxy, environment, inputs, paths, globs, and custom sources. These are trusted choices unless derived from lower-authority event data. Selecting a custom manifest delegates metadata and executable authority; selecting a path authorizes normal operations on it and intended referents. |
|
||||
| Project authors and pull-request contributors | May control project/version files, interpreter discovery state, virtual environments, symlinks, and cache inputs. This is ordinary project authority on trusted refs, but attacker authority when unreviewed content runs with secrets, write/OIDC/artifact authority, or persistent state. |
|
||||
| Remote metadata and artifacts | Default official endpoints, TLS roots, and an operator proxy are trusted mutable authorities. A custom manifest authorizes its URLs and hashes; a hash supplied by that same authority detects corruption, not malice. |
|
||||
| Cache and runner-state producers/consumers | Same-principal state is trusted by default. Integrity attacks require a lower-authority producer and higher-authority consumer. Confidentiality can flow the opposite way because lower-authority refs may read eligible higher-authority caches. Shared self-hosted state creates a boundary only when principals and authority differ. |
|
||||
| GitHub-managed automation | Dependency, coding-agent, and review workflows may exist outside the committed tree. Treat them as external principals and obtain their effective trigger, actor, token, environment, ref, and write/secret authority from live evidence. |
|
||||
|
||||
### Primary trust boundaries
|
||||
### Assumptions
|
||||
|
||||
| Boundary | Required property |
|
||||
|---|---|
|
||||
| Consumer workflow -> published action | The ref must resolve to the intended `action.yml` and bundles. A SHA or currently protected immutable tag is stronger than a mutable ref; verify actual protection. |
|
||||
| Project checkout -> runtime | Project files, Python state, paths, globs, and links may exercise documented project authority, not silently acquire workflow authority. |
|
||||
| Manifest -> artifact selection | Version, platform, variant, URL, archive metadata, and SHA-256 must parse fail-closed, bind one identity, and not silently expand credential recipients. |
|
||||
| Transport -> tool cache | Hashes required by policy must precede extraction; extraction/cache placement must remain contained; mirror fallback must preserve identity. |
|
||||
| Existing tool cache -> job | A reused name/version/architecture entry must share the job's trust domain or be independently validated before higher-authority execution. |
|
||||
| `uv python find` -> action | The returned path is executed; it must not be independently lower-authority-controlled when early execution matters. |
|
||||
| Action -> later steps | `PATH`, `UV_*`, `VIRTUAL_ENV`, outputs, state, and the problem matcher must remain data and resolve to intended paths/artifacts. |
|
||||
| GitHub cache service <-> eligible refs | Keys, scope, producers, consumers, restore paths, executable content, and data sensitivity must be safe in both integrity and confidentiality directions. |
|
||||
| Later steps -> post action | Changed environment/config/cache state must not turn post processing into a secret archiver, destructive prune, or persistence primitive. |
|
||||
| Remote manifest -> checksum updater | Rendered strings must remain data; generated source must be reviewable and must not become write-authority code execution. |
|
||||
| Release input -> tags/releases | Only the intended accepted commit may be published or tagged; target checks and current movement protections must hold. |
|
||||
| Dynamic automation -> repository/workflow state | Infer permissions from live evidence, not display names or uncommitted workflow definitions. |
|
||||
- Running the selected `uv` is intended. Later dependency or project execution is out of scope unless this action executes it earlier than intended, bypasses an isolation promise, or grants incremental authority.
|
||||
- Mutable official manifests, ranges, `latest`, and unprotected refs are not attacker control. A protected ref or independent checksum matters only if the selected bundle actually enforces it.
|
||||
- Same-user changes to paths, environment, proxies, or tool/cache state are not separate attacks. Demonstrate a cross-principal or lower-to-higher boundary.
|
||||
- Authorized paths include expected symlink/junction referents. Absolute paths and paths outside the workspace are supported; an escape requires independent control crossing an unauthorized boundary.
|
||||
- Hosted runners are assumed ephemeral and isolated. Persistence or hostile co-tenancy on self-hosted runners must be demonstrated.
|
||||
- Branch/tag rules, environments, token defaults, cache visibility, fork policy, dynamic workflows, and runner allocation are external state. Re-query required approvals/checks, bypass actors, tag movement, deployment reviewers/principals, release targets, and effective permissions for each attack path.
|
||||
- Web-application classes such as sessions, CSRF, XSS, SQL injection, and tenant isolation are not applicable.
|
||||
|
||||
### Assumptions, current controls, and exclusions
|
||||
### Security invariants
|
||||
|
||||
- The action is not setuid; it has exactly the workflow job and runner account's authority.
|
||||
- Running the selected `uv` is intended. Later dependency/project execution through `uv sync`, `uv run`, `uv pip install`, build backends, or tests matters here only if the action executes it earlier than intended, bypasses documented isolation, or grants incremental authority.
|
||||
- `latest`, ranges, maintained endpoints/manifests, and unprotected refs are intentionally mutable; mutability is not attacker control. A SHA/protected tag binds the action. A checksum binds downloaded bytes only when download validation runs; a tool-cache hit bypasses it and relies on cache provenance.
|
||||
- A malicious custom manifest is normally within delegated authority. It becomes a finding if a lower-authority actor controls it, it receives credentials outside that delegation, or it bypasses an independent checksum/origin promise.
|
||||
- Same-user changes to tool/uv/Python caches, `PATH`, `HOME`, XDG/proxy variables, or selected paths are not separate attacks. Demonstrate cross-account/repository/container authority or lower-to-higher reuse.
|
||||
- Authorized paths include normal symlink/junction referents. Absolute paths and globs outside `GITHUB_WORKSPACE` are supported; an escape requires an independently controlled path/link crossing an unauthorized boundary.
|
||||
- GitHub-hosted runners are assumed ephemeral and isolated; explicitly prove persistence or hostile co-tenancy on self-hosted runners.
|
||||
- SQL/template injection, XSS, CSRF, browser-origin, authentication/session, and tenant-isolation bugs are inapplicable because there is no web app or database.
|
||||
- A deliberately malicious ref/proxy/workflow author or compromise of maintainers, GitHub, Astral, or another declared trust root establishes impact, not by itself a repository vulnerability.
|
||||
1. **Published runtime:** review `action.yml`, committed `dist/*.cjs`, and runner-interpreted shipped files; source-only fixes do not protect consumers.
|
||||
2. **Executable identity:** precedence is workflow version, version file, project configuration, then `latest`. Manifest authority, platform, variant, URL, checksum, mirror fallback, extraction, and cache placement must bind the intended artifact. A tool-cache hit bypasses download validation and depends on cache provenance.
|
||||
3. **Credential recipients:** tokens and URL credentials may reach only workflow-authorized origins, redirects, paths, and logs. Metadata authority does not imply token-recipient authority.
|
||||
4. **Early execution:** before intended project execution, lower-authority content must not control interpreter discovery, cached tools, virtual environments, bare executable lookup, or native archive/cache helpers.
|
||||
5. **Paths and action channels:** path/environment changes, virtual-environment clearing, outputs, state, and problem matchers must affect only authorized targets and keep untrusted values as data.
|
||||
6. **Cache boundaries:** keys, scope, restore paths, and executable content must prevent lower-to-higher poisoning; cache contents and post-action path re-resolution must prevent higher-to-lower disclosure, destructive pruning, or persistence.
|
||||
7. **Workflow and release authority:** unreviewed code or mutable tooling must not acquire write, secret, OIDC, artifact, deployment, tag, or publication authority. Only the intended reviewed bundles and commit may be released.
|
||||
8. **Availability:** independently controlled manifests, archives, globs, traversal, and caches must stay within the accepted one-job resource-failure model.
|
||||
|
||||
Live controls rechecked on 2026-06-17:
|
||||
### Finding gate
|
||||
|
||||
| Control | Effective state |
|
||||
|---|---|
|
||||
| Default branch ruleset `branches-main` (`14390474`) | Active; blocks deletion/non-fast-forward, requires linear history and PRs, but requires zero approvals, no code-owner/last-push approval, no thread resolution, and no status checks. CI and `all-tests-passed` are evidence, not enforced merge/release prerequisites. |
|
||||
| Tag rulesets | Organization `tags-are-immutable` (`14390475`) blocks deletion, update, and non-fast-forward for all tags. Repository `tag-requires-release` (`14782916`) requires a successful `release` deployment. The queried principal reports no bypass. |
|
||||
| Release environments | `release` is main-only, disallows administrator bypass, and has no reviewer. Separate `release-gate` is main-only, disallows administrator bypass, and requires non-self Astral `Full-time` review. The workflow links them through dependencies; the tag ruleset requires only `release`. Inventory every principal able to produce that deployment. |
|
||||
| Releases | Published v8 releases report immutable through the Releases API; draft releases are not immutable. |
|
||||
| Other external configuration | Branch protection beyond the rules above, cache access, Actions allowlists, reusable-workflow policy, dynamic-workflow permissions, default token policy, fork approval, deployment principals, and runner allocation remain unverified until checked for a concrete path. |
|
||||
Before reporting, identify the attacker and victim principals; exact controlled input; scanned action and checkout refs; runtime reachability in committed bundles; effective token, secrets/OIDC, environment gates, cache scope, and runner persistence; applicable defaults and opt-ins; validation performed or skipped; declared trust roots; baseline versus incremental capability; and concrete impact. Reproduce platform-specific behavior and distinguish the scanned ref from other versions.
|
||||
|
||||
### Security invariants and finding gate
|
||||
|
||||
1. **Published code:** follow `action.yml` into committed `dist/*.cjs` and runner-interpreted artifacts such as `.github/python.json`; source-only fixes do not protect consumers.
|
||||
2. **Version authority:** precedence is `version` -> `version-file` -> `uv.toml`/`pyproject.toml` -> `latest`. On a miss, the manifest/checksum policy authorizes bytes; on a hit, tool-cache provenance does. Project data must not select an undeclared manifest/cache/local executable.
|
||||
3. **Artifact identity:** platform, architecture, version, default variant, URL, and checksum must bind one artifact. Fallback must preserve it; required hashes precede extraction. Hashes say nothing about an earlier cache hit.
|
||||
4. **Credential recipient:** tokens and URL credentials may reach only authorized origins, redirect targets, paths, and logs. Metadata authority does not automatically authorize token receipt.
|
||||
5. **Extraction:** entries, names, links, and metadata must remain inside the authorized temporary/cache root and produce the expected executable; evaluate platform-specific tar/ZIP behavior.
|
||||
6. **Tool cache:** a hit skips network and hash validation; use same-authority state or independent identity binding.
|
||||
7. **Early execution:** before intended project execution, lower-authority content must not control `ldd`, cached `uv`, `uv python find` results, `.venv`, `.python-version`, `PATH`, extraction helpers (`tar`, `unzip`, `pwsh`, `powershell`), or cache helpers (`gtar`, `zstd`, `zstdmt`, `unzstd`).
|
||||
8. **Path mutation:** `PATH`, `UV_TOOL_DIR`, `UV_TOOL_BIN_DIR`, `UV_PYTHON_INSTALL_DIR`, `UV_CACHE_DIR`, `VIRTUAL_ENV`, and venv clearing affect only operator-authorized paths; GitHub commands and child argv keep values as data.
|
||||
9. **Cache integrity:** scope/keying must prevent lower-authority executable or identity-confused content reaching a higher-authority consumer; managed-Python caches are executable code.
|
||||
10. **Cache confidentiality/post state:** do not cache private data visible to lower-authority eligible refs. Because post calls `loadInputs()` again, changes to `UV_CACHE_DIR`, `UV_PYTHON_INSTALL_DIR`, or cache-dir in `version-file`/`uv.toml`/`pyproject.toml` must not retarget pruning/saving to sensitive roots.
|
||||
11. **Workflow tokens:** unreviewed code or mutable tooling must not receive write, release, package, secret, OIDC, or artifact authority; evaluate effective permissions on the exact trigger/fork.
|
||||
12. **Release:** only reviewed bundles at the intended commit may be published/tagged. Immutable tags, required deployments, environments, and repeated draft-target checks must fail closed; checksum/docs automation must not turn remote data into code or bypass review.
|
||||
13. **Availability:** independently controlled manifests, archives, globs, traversal, or caches must not exceed the accepted one-job failure model through unbounded size/count/expansion/work.
|
||||
|
||||
Before reporting a finding, record: attacker and victim principals; the exact controlled file, URL, manifest/archive/cache field, environment value, event, or input; trigger, checkout/action refs, effective token, secrets/OIDC, environment gates, runner persistence, and cache scope; reachability in the committed bundle; scanned commit and whether each mitigation exists there rather than only on live `main`; affected trust root; documented behavior, precedence, defaults, and opt-ins such as custom manifests, absolute paths, Python caching, or activation; validation applied on downloads and skipped on tool/GitHub-cache hits; baseline versus incremental capability; concrete sink/impact; and current platform evidence, treating fixed historical behavior as already fixed. Uv-cache restore/save is a GitHub-hosted default (`enable-cache: auto`, `restore-cache: true`, `save-cache: true`), while Python caching is opt-in. Missing independent control, violated guarantee, reachability, incremental capability, or practical impact yields `NOT_APPLICABLE`, `INTENDED_BEHAVIOR`, `CORRECTNESS`, `DEFENSE_IN_DEPTH`, or `NEEDS_EVIDENCE`; decide security applicability before implementation quality.
|
||||
Missing independent attacker control, a violated guarantee, committed-runtime reachability, incremental capability, or practical impact is `NOT_APPLICABLE`, `INTENDED_BEHAVIOR`, `CORRECTNESS`, `DEFENSE_IN_DEPTH`, or `NEEDS_EVIDENCE`, not a security severity.
|
||||
|
||||
## Attack Surface, Mitigations, and Attacker Stories
|
||||
|
||||
### 1. Published action and dependency supply chain
|
||||
|
||||
Every consumer executes the selected ref's committed Node bundle, including bundled npm dependencies even though consumers do not run `npm install`; a malicious bundle can read job state, alter the workspace/environment, download code, and influence later steps. Controls include committed bundles, lockfile integrity hashes, `npm ci --ignore-scripts`, `npm run all`, generated-diff checks, generally full-SHA-pinned workflow actions, README SHA pins, and CommonJS output for the declared runtime. The live branch ruleset does not enforce approval or status checks.
|
||||
|
||||
Relevant paths are source/bundle divergence, compromised build dependencies, or publishing a commit other than the reviewed target. Impact requires an actual bypass of review, CI, branch, or release controls. A full SHA has Git-object immutability; a tag has comparable movement resistance only while its protections remain effective, so moving a currently protected tag is a control bypass rather than intended mutability.
|
||||
|
||||
### 2. Version and project configuration
|
||||
|
||||
`src/version/*` reads explicit inputs/files, `uv.toml`, `pyproject.toml`, requirements text, and `.tool-versions`; exact versions are direct, while `latest` and ranges query the selected manifest. Centralized precedence, semver/PEP 440 parsing, exact platform matching against manifest-listed artifacts, TOML/constrained-text parsing, argument passing without shell interpolation, and visible warn/fail/fallback-to-`latest` behavior constrain this surface.
|
||||
|
||||
An untrusted project may intentionally select a different official version. That is expected project authority unless a protected workflow independently promises a fixed tool. Report parser/precedence escape to arbitrary executables or a violated fixed-version guarantee, not merely selection of an older valid release.
|
||||
|
||||
### 3. Manifests, proxies, and SSRF
|
||||
|
||||
`src/download/manifest.ts` buffers and parses newline-delimited JSON; `src/utils/fetch.ts` honors HTTP(S) proxy state; `manifest-file` accepts any URL; records select version, platform, variant, artifact URL, archive metadata, and hash. Non-success, empty, legacy-array, missing/non-primitive-field, unsupported-platform, and ambiguous-default responses fail; cache is per URL and process. HTTPS and configured trust roots protect defaults. Live `main` has a five-second manifest abort timeout; checkout `88aa608` and versions before `8dc20b2acad09e25bb417df956dca0b8ecef365e` do not.
|
||||
|
||||
If lower-authority issue, pull-request, matrix, or other event data constructs `manifest-file`, it may probe runner-reachable services, drive unbounded parsing, select attacker bytes, or influence later token routing. Explicit operator selection delegates those powers. A hostile proxy or default-manifest compromise is a trust-root failure unless an independent promised origin/hash should still block it.
|
||||
|
||||
### 4. Artifact URLs, tokens, mirrors, and redirects
|
||||
|
||||
Official GitHub release URLs are preferentially rewritten to `releases.astral.sh`; current `main` exposes `download-from-astral-mirror` to disable that preference and `quiet` to suppress information-level URL logs. Mirror rewriting requires the exact official prefix; failure retries the manifest-selected original artifact with the same checksum decision. This is a secret boundary because the default token can authorize private GitHub data or repository operations.
|
||||
|
||||
Current `main`'s `githubTokenForUrl` (added in `853401723d6d6622f431a3b4e6385bf65e8035b7`) sends the token only to origin `https://github.com`, excluding mirrors, custom origins, malformed URLs, and lookalikes. Checkout `88aa608` and older bundles send it to every non-mirror artifact URL, including custom hosts. Both inspected bundles' `@actions/http-client` remove `Authorization` on hostname-changing redirects and reject HTTPS-to-HTTP downgrades by default. GitHub masks registered secrets, but consumers should minimize token permissions.
|
||||
|
||||
URL control always permits serving executable bytes. Historical bundles may also disclose the token directly. A current token leak must defeat origin parsing, reach an unauthorized same-host sink, bypass redirect controls, or use separately supplied URL credentials; ordinary cross-host redirects strip authorization. Logged URL credentials matter only if usable and unauthorized. Selecting a custom host delegates executable authority, not—on current `main`—GitHub-token authority.
|
||||
|
||||
### 5. Checksums, extraction, and executable placement
|
||||
|
||||
For default downloads, explicit `checksum` wins, then committed known checksums refreshed by repository automation; the default manifest's hash is not used directly, and absence of both skips validation. For custom manifests, explicit checksum wins, then the manifest hash. Exact SHA-256 comparison precedes tar/ZIP extraction and fails on mismatch; a failed mirror does not disable it. The custom-manifest parser accepts an empty hash string, which falls back to a matching built-in hash or no validation. A same-authority manifest hash does not constrain a malicious manifest.
|
||||
|
||||
`@actions/tool-cache` handles extraction and copies the resulting directory into the runner tool cache: Windows tries tar then ZIP; other platforms use tar and expect the official top-level directory. Native helpers come from `PATH`, so the toolkit does not authenticate them. Relevant classes are required-hash bypass, platform/version/representation mix-up, fallback identity change, archive traversal/link escape, caching a different executable than the validated archive, and unvalidated tool-cache reuse. Absence of an independent hash for an explicitly trusted custom source is not automatically a finding; archive-format strings alone confer no capability because that field is documented as ignored.
|
||||
|
||||
### 6. Runner tool cache and self-hosted persistence
|
||||
|
||||
`tryGetFromToolCache` searches name, requested/evaluated version, and architecture, then returns before download/hash validation. Partitioning and ephemeral hosted runners limit reuse; self-hosted persistence is intentional and documented.
|
||||
|
||||
A lower-authority job on a shared runner may prepopulate or mutate an entry later executed by a privileged job only if principals differ, scheduling and permissions permit the handoff, and the victim gains incremental secrets or authority. Same-account or administrator replacement is already local execution, not a separate finding.
|
||||
|
||||
### 7. Interpreter discovery and project-state execution
|
||||
|
||||
With no explicit `python-version`, `getPythonVersion` runs `uv python find --directory <working-directory>`, treats stdout as a path, and executes it with `--version` before cache restore. Project `.python-version`, `.venv`, local interpreters, and uv discovery rules can therefore cause early execution. Supplying `python-version` avoids this path. `@actions/exec` uses argv arrays; discovery failure is caught and produces cache-key version `unknown` instead of failing setup. `activate-environment` runs `uv venv` in project context, and `no-project` applies only to that command.
|
||||
|
||||
This matters in `pull_request_target`, privileged reusable/signing workflows, or shared-runner jobs that otherwise avoid project execution. It is not incremental if the same actor already controls an earlier equal-authority step or the workflow immediately runs the same checkout. Demonstrate actual uv discovery and returned path, not just a `.python-version` or `.venv`.
|
||||
|
||||
### 8. PATH, environment, venv clearing, and action channels
|
||||
|
||||
The action prepends installed/tool/user/Python/venv directories, exports `UV_*` and `VIRTUAL_ENV`, emits outputs/state, registers `.github/python.json` for the runner to interpret as annotation regexes/captures, invokes bare `ldd` and then bare `uv`, and lets bundled libraries resolve `tar`, `unzip`, `pwsh`, `powershell`, `gtar`, `zstd`, `zstdmt`, and `unzstd`. `uv venv` uses `--clear`. `UV_NO_MODIFY_PATH` disables most path changes and conflicts with activation; most explicit paths are resolved/normalized; argv and `@actions/core` reduce shell/command injection. The matcher path is fixed inside the selected action checkout.
|
||||
|
||||
Lower-authority writable search directories can replace helpers before validated artifact installation, during cache restore/save, or in later steps; a hostile venv path can cause destructive clearing; control/newline characters can spoof annotations or action files; writable prepended paths can shadow privileged tools. Require split authority and reproduce platform discovery/path/junction behavior. Trusted-runner lookup, operator-selected destructive paths, and same-user writable directories are not independently reportable.
|
||||
|
||||
### 9. GitHub uv and managed-Python caches
|
||||
|
||||
`src/cache/restore-cache.ts` derives an exact key from format version, architecture, platform, OS version, detected Python, prune/Python flags, dependency-content hash, and suffix. Restore has no prefix fallback; GitHub supplies scope and immutable keys; platform/OS partitioning reduces binary cross-use. With defaults, hosted runners restore and save uv caches; self-hosted caching is off unless enabled, and Python caching is separately opt-in. Globs may intentionally hash outside the workspace.
|
||||
|
||||
The success-only post action reloads inputs, optionally runs the saved absolute `uv` as `uv cache prune --ci`, and saves re-resolved paths. A later `UV_CACHE_DIR` or, for Python caching, `UV_PYTHON_INSTALL_DIR` change retargets save; the Python retarget lacks the uv-cache mismatch warning. Config-derived cache paths do not export `UV_CACHE_DIR`, so mutating the selected version file, `uv.toml`, or `pyproject.toml` retargets save and can also retarget prune when uv resolves that config in the post context, including the default working directory. Same-key uv hits skip overwrite/prune; missing paths fail or warn according to inputs.
|
||||
|
||||
Integrity attacks require a lower producer, matching scope/key/version, and higher consumer; managed-Python content is executable. Confidentiality flows the other way: GitHub's [dependency-caching documentation](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching) warns against secrets and permits fork PRs to access eligible base caches. Project input may also force detected Python to `unknown`, broaden globs, or change dependency hashes. Post retargeting needs a cache miss and successful job, plus incremental disclosure, destruction, or persistence. Untrusted code already holding the same secrets and unrestricted network often gains no new confidentiality, but cross-job persistence, lower-authority cache readers, or destructive pruning may.
|
||||
|
||||
### 10. Availability, parsing, and logging
|
||||
|
||||
The action buffers full manifests, parses all nonempty lines, enumerates versions, downloads/expands archives, scans and hashes broad globs, and uploads caches; remote strings and errors reach logs. There are no repository-level limits on manifest bytes/records, archive expansion, matched files, or cache size beyond libraries/runtime/services. Parsing rejects malformed records, ambiguity, unsupported platforms, and hash mismatch and never executes JSON as code. Failures normally affect one job. Current `main` adds the five-second manifest timeout and top-level uncaught setup/post handlers; `88aa608` lacks both.
|
||||
|
||||
Remote manifests/archives can exhaust memory, disk, CPU, or time; project globs can traverse large filesystems; strings can spoof logs; caches can exhaust quota. Medium needs independent control and reliable material cost. Operator-selected broad globs, malformed trusted files, and isolated job failures are usually correctness or Low.
|
||||
|
||||
### 11. Repository CI, updates, and releases
|
||||
|
||||
`.github/workflows/test.yml` executes PR code in builds/tests/the local action with default `contents: read`, disabled checkout credential persistence, and `security-events: write` in lint; one test installs a mutable `gh-act` extension and mutable container image with the job token. It otherwise uses SHA-pinned actions, `npm ci --ignore-scripts`, generated-bundle checks, actionlint, zizmor, and an `all-tests-passed` job aggregating every test. The CodeQL workflow, `.github/workflows/codeql-analysis.yml`, runs init/autobuild/analyze on PRs/main with pinned actions, disabled credential persistence, `actions: read`, `contents: read`, and `security-events: write`; current TypeScript-only `source-root` makes autobuild low-risk, subject to exact fork permissions.
|
||||
|
||||
`update-known-checksums.yml` runs on schedule/dispatch/`repository_dispatch` with contents/PR write, executes committed updater code before dependency install, consumes the mutable default manifest, renders TypeScript, rebuilds/validates, and pushes main or opens a PR. `update-docs.yml` reacts to version tags and writes docs; Release Drafter writes drafts on main. Committed privileged workflows pin third-party actions. Dynamic Dependabot/Copilot definitions and effective permissions remain external.
|
||||
|
||||
`release.yml` constrains the version pattern; validation and publication each require a draft still targeting `GITHUB_SHA`. Its DAG is validation -> reviewed `release-gate` -> `release` deployment -> publication. The tag ruleset requires only the deployment, not `release-gate`; `validate-release` requests contents-write before either environment although its current script only reads metadata. A release attack must identify every principal able to deploy `release`, distinguish ruleset bypass from workflow-DAG bypass, and defeat target checks/publication authority. Current tags cannot move under active rules, but a tag creator can trigger docs writes.
|
||||
|
||||
PR authors intentionally execute code in test jobs; severity depends on effective write/security-upload/secret/OIDC/persistence/artifact authority. A compromised versions manifest can attempt generated-source injection or misleading hashes only if it is outside the declared trust root or an independent control should stop it. Pre-gate write permission matters only if lower-authority input reaches executable workflow logic or another write sink. Mutable test tooling and an unprotected Copilot environment are signals, not High findings without durable privileged output or effective authority.
|
||||
|
||||
### 12. Lower-priority or non-applicable classes
|
||||
|
||||
- Runtime child processes use argv arrays, constraining shell injection; workflow `run:` blocks still require control and quoting analysis.
|
||||
- Prototype pollution/unsafe deserialization needs a dangerous merge or code sink; `JSON.parse` and typed fields alone do not establish it.
|
||||
- Secret-shaped fixture/lockfile/bundle/log strings need proof of a genuine, usable value disclosed to an unauthorized principal.
|
||||
- Documentation drift, unsupported variants, range surprises, cache misses, and malformed trusted configuration are correctness unless they cross an explicit integrity boundary.
|
||||
| Surface | Security-relevant behavior and controls | Reportable attacker story |
|
||||
|---|---|---|
|
||||
| Published action and build/release supply chain | Consumers execute committed bundles and embedded dependencies. Verify source/bundle alignment, lockfile integrity, dependency-install policy, reproducible/generated-diff checks, immutable action pins, branch enforcement, and publication target checks. | A lower-authority contributor or dependency changes shipped code, or release automation publishes a different commit, by bypassing an effective review, branch, or release control. |
|
||||
| Version, manifest, proxy, and network selection | Project files may select an official version by documented precedence. Custom manifests may select URLs, hashes, variants, and platforms and may reach arbitrary network locations. Parsing should reject malformed, ambiguous, unsupported, or incorrectly typed records; verify HTTPS, time/size bounds, proxy behavior, and selected-ref defaults. | Lower-authority event/project data violates a promised fixed version, escapes the selected manifest, probes runner-only services, causes material resource use, selects attacker bytes, or redirects later credentials. Operator selection of a custom authority is not itself a finding. |
|
||||
| Artifact URL, token, checksum, extraction, and tool cache | Mirror fallback must preserve identity and checksum policy. Origin gating should restrict tokens; redirect handling should strip authorization across unauthorized hosts and reject downgrade. Verify checksum precedence and reject missing/empty hashes when policy requires validation. Independent hashes must precede extraction. Native helpers come from `PATH`; tool-cache hits skip network/hash validation. | An attacker receives a usable token outside delegated authority, bypasses an independent pin, exploits archive/link traversal, substitutes the cached executable, or poisons shared tool state later executed with higher authority. Same-authority manifest hashes and same-user cache changes do not establish the boundary. |
|
||||
| Interpreter, PATH, virtual environment, and action channels | Without an explicit Python version, interpreter discovery can return a project-controlled path that is executed before cache restore. Project-isolation options for venv creation may not cover discovery. The action also changes later-step paths/environment, clears selected venv roots, emits state/outputs, registers a matcher, and invokes bare/native helpers. | Lower-authority project or shared-runner content executes before intended project code, destructive clearing crosses an unauthorized path boundary, writable search paths shadow privileged tools, or control characters alter action files/annotations. Require split authority and platform reproduction. |
|
||||
| GitHub uv/Python caches and post action | Cache keys should partition platform, interpreter, dependency, and policy state and restore without unsafe fallback. Determine cache defaults, visibility, and the exact hit/miss path from the selected ref and GitHub policy; an exact hit may suppress post save/prune. Post processing re-reads inputs/config/environment and may save re-resolved uv or Python paths. | A lower producer supplies executable content to a higher consumer; a higher producer exposes private data to a lower cache reader; or a later successful step retargets a cache miss toward sensitive files, destructive pruning, or cross-job persistence. Existing equal-authority code with the same secrets often gains no new confidentiality. |
|
||||
| CI, updater, dynamic automation, and release workflows | PR workflows intentionally execute contributor code. Verify effective permissions, fork behavior, credential persistence, mutable tooling, security-upload authority, and whether checks are required. Updaters convert remote data into source under write authority. Distinguish ruleset-required deployment from human review present only in a workflow DAG. | Unreviewed code gains write/secret/OIDC/artifact authority; remote metadata becomes executable generated source; a dynamic workflow has unexpected authority; or an actor satisfies a deployment/tag rule without the intended review and publishes a malicious ref. |
|
||||
| Availability and logging | Manifests, version enumeration, archives, globs, hashing, caches, and remote strings can consume resources or influence logs. Verify size/count/expansion bounds, timeouts, retries, top-level error handling, and that parsing never executes data. | Independently controlled input causes reliable material workflow cost, disk/memory exhaustion, or meaningful log/output manipulation. A bounded one-job failure or operator-selected broad input is usually Low or correctness. |
|
||||
| Lower-priority classes | Shell injection is constrained where child execution uses argv, but workflow shell blocks still require quoting review. Prototype pollution requires a dangerous merge/sink. Secret-shaped strings require proof of a genuine usable secret. Documentation drift, range surprises, malformed trusted config, and test-only code normally lack a security boundary. | Report only when a concrete lower-authority value reaches an execution, credential, persistent-state, publication, or material-availability sink. |
|
||||
|
||||
## Severity Calibration (Critical, High, Medium, Low)
|
||||
|
||||
Severity follows the complete attack graph and incremental capability, not keywords such as token, checksum, cache, manifest, archive, Python, PATH, release, or OIDC.
|
||||
Severity follows the complete attack graph and incremental capability, not the presence of words such as token, checksum, cache, manifest, archive, Python, PATH, release, or OIDC.
|
||||
|
||||
### Critical
|
||||
| Severity | Threshold | Representative examples |
|
||||
|---|---|---|
|
||||
| **Critical** | A low-prerequisite remote/lower-authority attacker compromises default distribution or installation across many consumers, publishes trusted malicious action artifacts, or gains broad credentials/runner control under safe defaults without first compromising a declared trust root. | Bypass an effective hash/origin control to distribute an automatically executed malicious binary at scale; reach publication authority to ship malicious bundles or move trusted refs without required approval; exploit default-accepted archive content for host overwrite or cross-job execution across hosted runners. |
|
||||
| **High** | A demonstrated lower-authority input crosses an execution, confidentiality, integrity, or persistence boundary in a privileged job and gains substantial capability. | Early interpreter execution in a write/OIDC release job; shared cache poisoning later executed with secrets; high-value cache disclosure to an untrusted ref; usable write-token disclosure; independent-pin bypass; archive/cache escape into sensitive state. |
|
||||
| **Medium** | A real but constrained crossing causes limited credential/filesystem impact, reliable remote denial of service, scoped persistence, or premature execution in a realistic uncommon configuration. | Early execution in a read-only PR job; limited same-repository cache confusion or disclosure; reliable hosted-runner exhaustion; disclosure of a usable read-only private token; output manipulation without publication or high-value credentials. |
|
||||
| **Low** | A genuine weak boundary causes narrow disclosure, log/annotation spoofing, defense-in-depth weakness, exotic cache aliasing without a privileged consumer, or limited waste. | Confusing logs with no execution effect; bounded job failure; limited overwrite of nonexecuted cache data; disclosure of a path/URL without private data or follow-on capability. |
|
||||
|
||||
A low-prerequisite remote/lower-authority attacker compromises default distribution or executable installation across many consumers, publishes trusted malicious action artifacts, or gains broad credentials/runner control under safe defaults without first compromising a declared trust root. Examples:
|
||||
Trust-root compromise may have Critical impact but is not a repository Critical without a lower-authority path into that root or an independent control that should have survived. High requires exact trigger, refs, effective authority, sink, and committed runtime; it cannot rely only on a trusted operator choosing malicious inputs, same-user state changes, or code already intentionally executed with equal authority. A separate privileged consumer, broad secret, persistent trusted state, publication path, or cross-repository boundary can raise Medium to High.
|
||||
|
||||
- bypassing an effective hash/origin control to distribute an automatically executed malicious `uv` at scale;
|
||||
- reaching contents-write publication to ship malicious bundles or move trusted refs without required approval; or
|
||||
- using independently controlled default-accepted archive content for host-file overwrite or cross-job execution across hosted runners.
|
||||
|
||||
Compromise of maintainers, GitHub, Astral releases, or `astral-sh/versions` may have critical impact but is not a repository Critical without a lower-authority path into that root or an independent control that should have survived.
|
||||
|
||||
### High
|
||||
|
||||
A demonstrated lower-authority input crosses an execution, confidentiality, integrity, or persistence boundary in a privileged job and gains substantial capability. Examples:
|
||||
|
||||
- project interpreter discovery executes attacker code in a `pull_request_target`/release/signing job with write or OIDC before any intended project execution;
|
||||
- a lower-authority repository poisons shared tool/Python cache code later executed by a secret-bearing job;
|
||||
- a privileged cache exposes usable credentials/private source to a fork PR;
|
||||
- attacker-controlled URL/redirect data sends a usable write token to an unauthorized recipient;
|
||||
- a hash/identity bypass substitutes attacker bytes for an explicitly pinned artifact in a privileged job; or
|
||||
- archive/cache content escapes its root and overwrites sensitive runner, credential, workspace, or executable state.
|
||||
|
||||
Show the exact trigger, refs, permissions, secret/OIDC/persistence sink, and committed runtime. Trusted selection of a malicious custom manifest, same-user cache changes, or code already intentionally executed with equal authority is not High.
|
||||
|
||||
### Medium
|
||||
|
||||
A real but constrained crossing causes limited credential/filesystem impact, reliable remote denial of service, scoped persistence, or premature code execution in an uncommon realistic configuration. Examples:
|
||||
|
||||
- interpreter execution meaningfully manipulates a read-only PR job without write, secrets, OIDC, or cross-job authority;
|
||||
- same-repository/ref cache confusion executes or overwrites limited data;
|
||||
- eligible lower-authority PRs receive limited private protected-ref cache data;
|
||||
- an attacker manifest/archive reliably exhausts hosted resources without compromising a default trust root;
|
||||
- an unauthorized endpoint receives a usable read-only private-repository token; or
|
||||
- command/output manipulation changes later decisions/artifacts without publication or high-value credentials.
|
||||
|
||||
A privileged consumer, broad secret, persistent trusted state, publication path, or cross-repository boundary can raise these to High.
|
||||
|
||||
### Low
|
||||
|
||||
A genuine weak boundary causes narrow disclosure, log/annotation spoofing, defense-in-depth origin/path weakness, exotic cache aliasing without a privileged consumer, or limited waste. Examples are confusing logs with no execution/environment effect, bounded one-job failure or small resource increase, limited overwrite of nonexecuted shared cache data, or disclosure of a path/URL without credentials, private data, or follow-on capability.
|
||||
|
||||
### Informational, correctness, intended behavior, or not applicable
|
||||
|
||||
Without additional evidence, do not assign security severity to:
|
||||
|
||||
- expected mutability of `latest`, ranges, official/custom sources, or an explicitly selected unprotected ref (movement of a currently protected immutable tag is different);
|
||||
- documented project version selection;
|
||||
- deliberate workflow-author selection of a manifest, checksum, proxy, absolute/cache/venv root;
|
||||
- same-user/runner-admin cache changes without a cross-principal consumer;
|
||||
- selected `uv` or later dependency/project execution the workflow requested;
|
||||
- trusted-runner `PATH` lookup without a lower-authority writable directory;
|
||||
- links inside an authorized root without split-principal control;
|
||||
- test/developer-only code with no shipped or privileged-workflow path;
|
||||
- historical behavior fixed at the scanned commit; or
|
||||
- compatibility/documentation/correctness issues without incremental confidentiality, integrity, persistence, or availability impact.
|
||||
Normally non-reportable without additional evidence: expected mutability of ranges, `latest`, official/custom sources, or unprotected refs; documented project version selection; deliberate operator selection of manifests, proxies, checksums, or paths; same-principal cache/path changes; requested `uv` or dependency execution; trusted-runner `PATH` lookup; authorized symlink referents; test/developer-only code without a shipped or privileged-workflow path; behavior fixed in the scanned ref; and correctness/compatibility/documentation issues without incremental confidentiality, integrity, persistence, or availability impact.
|
||||
|
||||
Reference in New Issue
Block a user