From 9a60234e21143680d02b8f42cf381c7a316cdd0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 28 Oct 2026 00:47:04 -0700 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=93=9D=20docs(security):=20add=20a=20?= =?UTF-8?q?threat=20model=20and=20align=20the=20policy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/SECURITY.md | 50 ++++++++++++++++++++++--- .github/THREAT_MODEL.md | 83 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 .github/THREAT_MODEL.md diff --git a/.github/SECURITY.md b/.github/SECURITY.md index ae419c1..0f9eeaa 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,9 +1,49 @@ -# Security Policy +# Security policy -## Supported Versions +action-pre-commit-uv is a GitHub composite action that runs pre-commit with pre-commit-uv in a workflow. This policy +says how to report a security problem and how we decide whether a report is one. -Only the latest released version is supported with security updates. Older versions will not receive patches. +## Reporting a vulnerability -## Reporting a Vulnerability +**Do not open a public issue for a security problem.** Use +[GitHub's private vulnerability reporting](https://github.com/tox-dev/action-pre-commit-uv/security/advisories/new). If +neither works for you, email [gaborjbernat@gmail.com](mailto:gaborjbernat@gmail.com). -Please report security vulnerabilities via [GitHub's private vulnerability reporting](../../security/advisories/new). +Include what the attacker controls and what they gain, the steps or a proof of concept, and the versions and platform +you tested. Write the report yourself; unreviewed tool or model output takes longer to triage. + +## What to expect + +Volunteers maintain action-pre-commit-uv, with no on-call rotation. We aim to acknowledge a report within **5 working +days** and assess it within **15 working days**. Fixes are developed in a private fork attached to the draft advisory, +ship in a new release, and the advisory, with a CVE requested through GitHub, goes out with that release. The default +embargo is **90 days** from the report; we agree any change with the reporter. We credit reporters in the advisory under +the name they choose, or not at all. There is no bug bounty. + +## Supported versions + +Only the latest release receives fixes. Pin the action to a full commit SHA and let Dependabot move it. + +## Vulnerability or bug + +A report is a vulnerability when an attacker who controls only an input that the [threat model](THREAT_MODEL.md) lists +as untrusted breaks a guarantee action-pre-commit-uv makes. A report that first needs control of something the threat +model trusts, such as the workflow that calls the action, the repository's `.pre-commit-config.yaml`, or the runner, is +a bug: we fix it in the open and publish no advisory. Classify by who supplies the dangerous input, not by how bad the +outcome sounds. + +**In scope** + +- An input (`extra_args`, `uv-install`) reaching a shell as code rather than as data. +- The action running a tool version or source it does not name: the pinned `setup-uv`, `pre-commit` and `pre-commit-uv` + from PyPI. +- A cache written by one ref being restored for a ref GitHub's cache scoping should isolate. + +**Out of scope** + +- Hooks from `.pre-commit-config.yaml`: the action runs them as pre-commit would, so a workflow that runs it on + untrusted pull request code must not hold secrets, as with any job that runs that code. +- Choosing `pull_request_target` or granting the job write tokens; that is the calling workflow's decision. +- Vulnerabilities in pre-commit, pre-commit-uv, uv or `setup-uv`. + +If you cannot tell which side a finding falls on, report it privately and we will decide. diff --git a/.github/THREAT_MODEL.md b/.github/THREAT_MODEL.md new file mode 100644 index 0000000..33f21f6 --- /dev/null +++ b/.github/THREAT_MODEL.md @@ -0,0 +1,83 @@ +# Threat model + +This model says what action-pre-commit-uv trusts, where untrusted input enters, and which guarantees hold at each +boundary, so a report can be classified as a vulnerability or a bug. [SECURITY.md](SECURITY.md) covers reporting; if the +two disagree, SECURITY.md wins and we fix this file. + +## How action-pre-commit-uv works + +The action optionally installs uv with a SHA-pinned `astral-sh/setup-uv`, restores `~/.cache/pre-commit` keyed by the +interpreter and the hash of `.pre-commit-config.yaml`, and runs `pre-commit run` through `uv run` (with the project) or +`uvx` (without one), with `pre-commit-uv` added. `extra_args` reaches the shell only through an environment variable, +never through `${{ }}` interpolation in a script. + +```mermaid +flowchart LR + wf["Calling workflow, inputs
(trusted)"] --> act["action-pre-commit-uv"] + act -->|"setup-uv (SHA pinned)"| uv["uv"] + act <-->|"key: python + config hash"| cache[("Actions cache")] + repo[("Checked-out code, may be
an untrusted PR")] --> pc["pre-commit run"] + act --> pc + classDef trusted fill:#cfe4ff,stroke:#1f6feb,color:#0b1f3a; + classDef boundary fill:#ffe3a3,stroke:#d29200,color:#3a2c00; + class wf,act,uv trusted; + class cache,repo,pc boundary; +``` + +## Compared with similar tools + +We drew the boundary where comparable projects in other ecosystems draw theirs. The shared rule, stated by the +[Node.js threat model](https://github.com/nodejs/node/blob/main/SECURITY.md#the-nodejs-threat-model), is that a report +which first needs a trusted element compromised is out of scope; the first triage question, as +[DOMPurify](https://github.com/cure53/DOMPurify/blob/main/SECURITY.md#what-we-consider-a-vulnerability) puts it, is what +the attacker controls. + +| Project | Language | Where it draws the line | +| ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- | +| [GitHub Actions hardening guide](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions) | Actions | Inputs must reach scripts through environment variables, not expression interpolation; untrusted code must not run with secrets. | +| [pre-commit/action](https://github.com/pre-commit/action) | Actions | The upstream action this one mirrors; it publishes no security policy. | +| [Lefthook](https://github.com/evilmartians/lefthook/security/policy) | Go | Runs the commands its configuration names by design; that is not a vulnerability. | + +## Trusted + +An attacker who controls any of these already has what action-pre-commit-uv could give them: + +- The calling workflow, its trigger, permissions and inputs. +- The runner, `setup-uv`, and the packages installed from PyPI. +- `.pre-commit-config.yaml` and its hooks, as far as the workflow chose to run them. + +## Untrusted inputs + +| Input | Comes from | What action-pre-commit-uv guarantees | +| ------------------------ | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `extra_args` | The calling workflow, sometimes built from event data | Passed through an environment variable, so it is split into words but never evaluated as shell code. | +| Cached hook environments | Earlier runs on other refs | GitHub scopes caches by branch; the key includes the config hash. | + +## Threats and controls + +| STRIDE | Threat | Control | +| --------- | -------------------------------------------------------------- | --------------------------------------------------------------------- | +| Tampering | `extra_args` containing \`; curl ... | sh\` runs as a command. | +| Tampering | A poisoned cache from a pull request feeds the default branch. | GitHub's cache scoping; the action writes only `~/.cache/pre-commit`. | + +## Release chain + +Releases are tags on this repository, created by the release App and published as immutable GitHub Releases. Every +third-party action it uses is pinned to a commit SHA. + +## Vulnerability or bug + +| Report | Verdict | Why | +| --------------------------------------------------------------------- | ------------------- | ------------------------------------------------------ | +| `extra_args: "--files $(id)"` runs `id` | Vulnerability | Input reached the shell as code. | +| A PR's hook steals `GITHUB_TOKEN` in a `pull_request_target` workflow | Not a vulnerability | The workflow chose to run untrusted code with secrets. | +| The cache misses on every run | Bug | Performance, no boundary crossed. | + +## Accepted risks + +- `extra_args` is word-split and glob-expanded by the shell, as documented for pre-commit arguments. + +## Keeping this current + +Revisit this model when action-pre-commit-uv gains a new input source, network access, a new way to run code, or a new +place it writes to, and whenever an advisory is published. From 3df56a51cad894351edfa58c79296618c6c73ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 28 Oct 2026 07:09:29 -0700 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20docs(security):=20check=20th?= =?UTF-8?q?e=20threat=20model=20against=20the=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/SECURITY.md | 4 ++-- .github/THREAT_MODEL.md | 38 ++++++++++++++++++++++++-------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 0f9eeaa..d4451b0 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -35,8 +35,8 @@ outcome sounds. **In scope** - An input (`extra_args`, `uv-install`) reaching a shell as code rather than as data. -- The action running a tool version or source it does not name: the pinned `setup-uv`, `pre-commit` and `pre-commit-uv` - from PyPI. +- The action running a tool it does not name: `setup-uv` at its pinned SHA, the uv release whose checksum that + `setup-uv` bundles, and pre-commit and pre-commit-uv from the configured index. - A cache written by one ref being restored for a ref GitHub's cache scoping should isolate. **Out of scope** diff --git a/.github/THREAT_MODEL.md b/.github/THREAT_MODEL.md index 33f21f6..fd42b3a 100644 --- a/.github/THREAT_MODEL.md +++ b/.github/THREAT_MODEL.md @@ -6,10 +6,12 @@ two disagree, SECURITY.md wins and we fix this file. ## How action-pre-commit-uv works -The action optionally installs uv with a SHA-pinned `astral-sh/setup-uv`, restores `~/.cache/pre-commit` keyed by the -interpreter and the hash of `.pre-commit-config.yaml`, and runs `pre-commit run` through `uv run` (with the project) or -`uvx` (without one), with `pre-commit-uv` added. `extra_args` reaches the shell only through an environment variable, -never through `${{ }}` interpolation in a script. +The action installs uv with a SHA-pinned `astral-sh/setup-uv`, at the newest release that `setup-uv` bundles a checksum +for, unless uv is already present (`uv-install`). It restores `~/.cache/pre-commit` keyed by the interpreter and the +hash of `.pre-commit-config.yaml`, and runs `pre-commit run` through `uv run` when the repository has a +`pyproject.toml`, or `uvx` when it does not, with the latest `pre-commit-uv` added. `extra_args` reaches the shell only +through an environment variable, never through `${{ }}` interpolation in a script; `uv-install` is used only in step +conditions. ```mermaid flowchart LR @@ -35,7 +37,7 @@ the attacker controls. | Project | Language | Where it draws the line | | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- | | [GitHub Actions hardening guide](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions) | Actions | Inputs must reach scripts through environment variables, not expression interpolation; untrusted code must not run with secrets. | -| [pre-commit/action](https://github.com/pre-commit/action) | Actions | The upstream action this one mirrors; it publishes no security policy. | +| [pre-commit/action](https://github.com/pre-commit/action) | Actions | The upstream action this one mirrors; reports go through Tidelift. | | [Lefthook](https://github.com/evilmartians/lefthook/security/policy) | Go | Runs the commands its configuration names by design; that is not a vulnerability. | ## Trusted @@ -43,22 +45,22 @@ the attacker controls. An attacker who controls any of these already has what action-pre-commit-uv could give them: - The calling workflow, its trigger, permissions and inputs. -- The runner, `setup-uv`, and the packages installed from PyPI. +- The runner, `setup-uv`, the uv release it installs, and the packages installed from PyPI. - `.pre-commit-config.yaml` and its hooks, as far as the workflow chose to run them. ## Untrusted inputs -| Input | Comes from | What action-pre-commit-uv guarantees | -| ------------------------ | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| `extra_args` | The calling workflow, sometimes built from event data | Passed through an environment variable, so it is split into words but never evaluated as shell code. | -| Cached hook environments | Earlier runs on other refs | GitHub scopes caches by branch; the key includes the config hash. | +| Input | Comes from | What action-pre-commit-uv guarantees | +| --------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `extra_args` | The calling workflow, sometimes built from event data | Passed through an environment variable, so it is split into words but never evaluated as shell code. | +| Cached hook environments and uv's cache | Earlier runs on other refs | GitHub scopes caches by branch; the pre-commit cache key includes the config hash. | ## Threats and controls -| STRIDE | Threat | Control | -| --------- | -------------------------------------------------------------- | --------------------------------------------------------------------- | -| Tampering | `extra_args` containing \`; curl ... | sh\` runs as a command. | -| Tampering | A poisoned cache from a pull request feeds the default branch. | GitHub's cache scoping; the action writes only `~/.cache/pre-commit`. | +| STRIDE | Threat | Control | +| --------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| Tampering | `extra_args` containing \`; curl ... | sh\` runs as a command. | +| Tampering | A poisoned cache from a pull request feeds the default branch. | GitHub's cache scoping; the action caches only `~/.cache/pre-commit` and, through `setup-uv`, uv's cache. | ## Release chain @@ -73,9 +75,17 @@ third-party action it uses is pinned to a commit SHA. | A PR's hook steals `GITHUB_TOKEN` in a `pull_request_target` workflow | Not a vulnerability | The workflow chose to run untrusted code with secrets. | | The cache misses on every run | Bug | Performance, no boundary crossed. | +## Severity + +- **High:** an input runs as shell code, or a cache from an untrusted ref is restored on a trusted one. +- **Medium:** the action runs a tool from a source other than the ones it names. +- **Low:** a step fails or misses the cache without crossing a boundary. + ## Accepted risks - `extra_args` is word-split and glob-expanded by the shell, as documented for pre-commit arguments. +- pre-commit and pre-commit-uv are not version-pinned: each run takes the latest releases. uv is the newest release the + pinned `setup-uv` knows (`latest-known`), checked against its bundled checksum. ## Keeping this current