From ebcfb4a6eba1640698ee76ae674c7b4615f5ba06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 28 Oct 2026 00:46:58 -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 | 55 +++++++++++++++++++++----- .github/THREAT_MODEL.md | 85 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 .github/THREAT_MODEL.md diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 83b7aca..0cbb7ee 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,13 +1,50 @@ -# Security Policy +# Security policy -## Supported Versions +pre-commit-uv is a pre-commit add-on that builds Python hook environments with uv in place of virtualenv and pip. This +policy says how to report a security problem and how we decide whether a report is one. -| Version | Supported | -| ------- | ------------------ | -| 4+ | :white_check_mark: | -| <4 | :x: | +## 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/pre-commit-uv/security/advisories/new). If you do +not have a GitHub account, or you are a Tidelift subscriber, report through the +[Tidelift security contact](https://tidelift.com/security) instead; Tidelift coordinates the fix and the disclosure with +us. If neither works for you, email [gaborjbernat@gmail.com](mailto:gaborjbernat@gmail.com). -To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift -will coordinate the fix and disclosure. +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 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. Confirm the problem reproduces on it before reporting. + +## 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 pre-commit-uv makes. A report that first needs control of something the threat model +trusts, such as the `.pre-commit-config.yaml`, the hook repositories it pins, or the bundled uv, 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** + +- pre-commit-uv installing something other than what pre-commit asked for: a different hook repository, revision or + `additional_dependencies`. +- pre-commit-uv running a `uv` other than the bundled `uv` package. +- The patch activating when `DISABLE_PRE_COMMIT_UV_PATCH` is set. + +**Out of scope** + +- Hooks and their dependencies: the config chooses them and pre-commit runs them; a malicious hook repository is trusted + by the config that pins it. +- pre-commit's own behavior; report it to [pre-commit](https://github.com/pre-commit/pre-commit). +- uv's resolution and downloads; report those to [uv](https://github.com/astral-sh/uv/security/policy). + +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..4701f26 --- /dev/null +++ b/.github/THREAT_MODEL.md @@ -0,0 +1,85 @@ +# Threat model + +This model says what 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 pre-commit-uv works + +Installed next to pre-commit, pre-commit-uv patches pre-commit's Python language at startup, only when the running +program is pre-commit. When pre-commit installs a Python hook, the patch creates the hook's environment with `uv venv` +and installs the hook package and `additional_dependencies` with `uv pip install`, using the bundled `uv`. Everything +else, from cloning hook repositories to running hooks, stays with pre-commit. + +```mermaid +flowchart LR + cfg[(".pre-commit-config.yaml
(trusted)")] --> pc["pre-commit"] + pc -->|"install Python hook"| pcu["pre-commit-uv patch"] + pcu -->|"bundled uv: venv, pip install"| env["Hook environment"] + idx[("Indexes, hook repos")] --> env + pc -->|"run hook on files"| env + classDef trusted fill:#cfe4ff,stroke:#1f6feb,color:#0b1f3a; + classDef boundary fill:#ffe3a3,stroke:#d29200,color:#3a2c00; + class cfg,pc,pcu trusted; + class env,idx 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 | +| -------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------- | +| [pre-commit](https://github.com/pre-commit/pre-commit) | Python | The host tool publishes no security policy; it runs whatever hooks the config pins. | +| [Lefthook](https://github.com/evilmartians/lefthook/security/policy) | Go | Runs the commands its configuration names by design; that is not a vulnerability. | +| [uv](https://github.com/astral-sh/uv/security/policy) | Rust | The installer pre-commit-uv delegates to. | + +## Trusted + +An attacker who controls any of these already has what pre-commit-uv could give them: + +- The user, `.pre-commit-config.yaml`, and the hook repositories and revisions it pins. +- pre-commit and the bundled `uv`. +- The configured package indexes and `UV_*` settings. + +## Untrusted inputs + +| Input | Comes from | What pre-commit-uv guarantees | +| ------------------------- | ------------------------------------------ | --------------------------------------------------------- | +| The program being started | Any Python process importing site packages | The patch applies only when pre-commit itself is running. | + +## Threats and controls + +| STRIDE | Threat | Control | +| ---------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Tampering | The patch installs a hook package other than the one pre-commit requested. | It passes pre-commit's own package list to `uv pip install` unchanged. | +| Elevation of privilege | The patch loads into an unrelated program and changes how it installs. | Startup check that the caller is pre-commit; `DISABLE_PRE_COMMIT_UV_PATCH` turns it off. | + +## Release chain + +Releases run on the shared [tox-dev/github-workflows](https://github.com/tox-dev/github-workflows) pipeline. A release +App, not a person, pushes the release commit and tag; the build installs its tools hash-checked from `pylock.toml` with +a seven-day upload cooldown; PyPI receives the files through +[trusted publishing](https://docs.pypi.org/trusted-publishers/) with attestations, and the GitHub Release is immutable. +Every workflow pins third-party actions to a commit SHA. + +## Vulnerability or bug + +| Report | Verdict | Why | +| ---------------------------------------------------------------------------------------- | ------------------- | -------------------------------------------- | +| `pre-commit-uv` installs an `additional_dependencies` entry that the config did not list | Vulnerability | Install set must match pre-commit's request. | +| A pinned hook repository ships malware | Not a vulnerability | The config trusts its hooks. | +| uv picks a different Python than virtualenv would | Bug | Behavior difference, no boundary crossed. | + +## Accepted risks + +- pre-commit-uv inherits pre-commit's trust in hook repositories. + +## Keeping this current + +Revisit this model when 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 433ea1ab5513983c3b7b0431ffd3e1094b7a3599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 28 Oct 2026 07:09:25 -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/THREAT_MODEL.md | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/.github/THREAT_MODEL.md b/.github/THREAT_MODEL.md index 4701f26..75aa263 100644 --- a/.github/THREAT_MODEL.md +++ b/.github/THREAT_MODEL.md @@ -6,10 +6,12 @@ disagree, SECURITY.md wins and we fix this file. ## How pre-commit-uv works -Installed next to pre-commit, pre-commit-uv patches pre-commit's Python language at startup, only when the running -program is pre-commit. When pre-commit installs a Python hook, the patch creates the hook's environment with `uv venv` -and installs the hook package and `additional_dependencies` with `uv pip install`, using the bundled `uv`. Everything -else, from cloning hook repositories to running hooks, stays with pre-commit. +Installed next to pre-commit, pre-commit-uv ships a `.pth` file that runs at every interpreter start in that environment +and patches pre-commit's Python language only when the running program is pre-commit (or `FORCE_PRE_COMMIT_UV_PATCH` is +set) and `DISABLE_PRE_COMMIT_UV_PATCH` is not. When pre-commit installs a Python hook, the patch creates the hook's +environment with `uv venv` and installs the hook package and `additional_dependencies` with `uv pip install`, using the +bundled `uv` with `--project` pointing at the repository root. Everything else, from cloning hook repositories to +running hooks, stays with pre-commit. ```mermaid flowchart LR @@ -32,11 +34,11 @@ which first needs a trusted element compromised is out of scope; the first triag [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 | -| -------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------- | -| [pre-commit](https://github.com/pre-commit/pre-commit) | Python | The host tool publishes no security policy; it runs whatever hooks the config pins. | -| [Lefthook](https://github.com/evilmartians/lefthook/security/policy) | Go | Runs the commands its configuration names by design; that is not a vulnerability. | -| [uv](https://github.com/astral-sh/uv/security/policy) | Rust | The installer pre-commit-uv delegates to. | +| Project | Language | Where it draws the line | +| -------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------- | +| [pre-commit](https://github.com/pre-commit/pre-commit) | Python | The host tool runs whatever hooks the config pins; 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. | +| [uv](https://github.com/astral-sh/uv/security/policy) | Rust | The installer pre-commit-uv delegates to. | ## Trusted @@ -44,13 +46,14 @@ An attacker who controls any of these already has what pre-commit-uv could give - The user, `.pre-commit-config.yaml`, and the hook repositories and revisions it pins. - pre-commit and the bundled `uv`. -- The configured package indexes and `UV_*` settings. +- The configured package indexes, `UV_*` settings, and the repository's own uv configuration, which uv reads through + `--project`. ## Untrusted inputs -| Input | Comes from | What pre-commit-uv guarantees | -| ------------------------- | ------------------------------------------ | --------------------------------------------------------- | -| The program being started | Any Python process importing site packages | The patch applies only when pre-commit itself is running. | +| Input | Comes from | What pre-commit-uv guarantees | +| ------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| The program being started | Any Python process in the environment, since the `.pth` file runs at startup | The patch applies only when pre-commit itself is running or `FORCE_PRE_COMMIT_UV_PATCH` is set, and never with `DISABLE_PRE_COMMIT_UV_PATCH`. | ## Threats and controls @@ -62,8 +65,8 @@ An attacker who controls any of these already has what pre-commit-uv could give ## Release chain Releases run on the shared [tox-dev/github-workflows](https://github.com/tox-dev/github-workflows) pipeline. A release -App, not a person, pushes the release commit and tag; the build installs its tools hash-checked from `pylock.toml` with -a seven-day upload cooldown; PyPI receives the files through +App, not a person, pushes the release commit and tag; the build installs its tools from a hash-checked `pylock.toml` +resolved with a seven-day upload cooldown by default; PyPI receives the files through [trusted publishing](https://docs.pypi.org/trusted-publishers/) with attestations, and the GitHub Release is immutable. Every workflow pins third-party actions to a commit SHA. @@ -75,6 +78,12 @@ Every workflow pins third-party actions to a commit SHA. | A pinned hook repository ships malware | Not a vulnerability | The config trusts its hooks. | | uv picks a different Python than virtualenv would | Bug | Behavior difference, no boundary crossed. | +## Severity + +- **High:** pre-commit-uv installs a package pre-commit did not request, or runs a `uv` other than the bundled one. +- **Medium:** the patch changes how an unrelated program installs, or activates despite `DISABLE_PRE_COMMIT_UV_PATCH`. +- **Low:** a crash or confusing error while setting up a hook environment. + ## Accepted risks - pre-commit-uv inherits pre-commit's trust in hook repositories.