[agent] Found by the scheduled Hatch bug-hunt routine (ledger #314).
Summary
In a Hatch project whose environment uses installer = "uv", a [tool.uv.sources] entry for the patched package (for example a url, path or git source) takes precedence over the PEP 508 direct reference that hosted mode writes into [project].dependencies. Hatch installs the project with uv pip install -e ., and uv applies the project's tool.uv.sources. Hosted scan never looks at [tool.uv.sources]. It rewrites six==1.16.0 to six @ <patch url>#sha256=… and reports success, redirected: 1, exit 0. Then:
- with a
url or path source, every fresh Hatch env silently installs the unpatched upstream wheel;
- with a
git source, every Hatch env creation fails with Conflicting URLs for package six.
The docs say a source table is refused before anything is written, so the scan should have failed closed.
Impact
CI gets a green scan and a pyproject that looks patched, but the vulnerable code still ships. For git sources the project stops installing at all. vex does correctly omit six (not_applied) when it inspects the env, so the silent part is the scan result. Vendored mode on the same project refuses (with pypi_uv_no_lockfile), so it fails closed.
Repro (Hatch 1.18.1, uv 0.12, Linux; local mock patch API serving a patched six 1.16.0 wheel)
mkdir app && cd app && mkdir -p src/app && touch src/app/__init__.py
cat > pyproject.toml <<'EOF'
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "app"
version = "0.1.0"
dependencies = ["six==1.16.0"]
[tool.hatch.envs.default]
installer = "uv"
[tool.uv.sources]
six = { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl" }
EOF
socket-patch scan --mode hosted --yes --json --ecosystems pypi … # exit 0, status success, redirect.redirected = 1, no warnings
grep dependencies pyproject.toml # six @ http://…/patch/pypi/six/1.16.0/…/six-1.16.0-py2.py3-none-any.whl#sha256=…
hatch run python -c 'import six; print(getattr(six, "SOCKET_PATCHED", "UNPATCHED"))' # fresh env -> UNPATCHED
Swap the source for { path = "/abs/six-1.16.0-py2.py3-none-any.whl" } to get the same result. With { git = "https://github.com/benjaminp/six", tag = "1.16.0" }, hatch run fails with:
error: Requirements contain conflicting URLs for package `six`:
- git+https://github.com/benjaminp/six@1.16.0
- http://127.0.0.1:18080/patch/pypi/six/1.16.0/…/six-1.16.0-py2.py3-none-any.whl
Expected vs actual
- Expected: docs/testing/hatch.md: "A range, transitive-only declaration, dynamic dependency metadata, custom environment plugin, source table or conditional override is refused before writing. Use agent mode…". A uv source for a package Hatch installs through uv should be refused (or at least warned about) the same way Hatch
sources are, with nothing written and a non-zero or warning result.
- Actual: exit 0
success, redirected: 1, no warning. The rewrite is either ignored (url/path) or breaks installs (git).
Matrix (Linux, each cell run twice)
| Shape |
Hatch 1.16.5 |
Hatch 1.18.1 |
installer = "uv", project deps, tool.uv.sources url |
fail (unpatched) |
fail (unpatched) |
installer = "uv", project deps, tool.uv.sources path |
fail (unpatched) |
fail (unpatched) |
installer = "uv", project deps, tool.uv.sources git |
fail (install error) |
fail (install error) |
installer = "uv", features = ["x"] (optional deps), url source |
— |
fail (unpatched) |
installer = "uv", env dependencies, url source |
— |
pass (env reqs are passed to uv as plain strings, so project sources don't apply) |
installer = "uv", tool.uv.sources index source (explicit index) |
— |
pass (the direct URL wins) |
| pip installer (default), url source |
— |
pass (pip ignores tool.uv.sources) |
macOS / Windows: not probed (probe branches can't be deleted from this sandbox right now). The rewrite is OS-independent.
Suspect code
crates/socket-patch-core/src/utils/hatch.rs:247 refuses only Hatch's own tool.hatch.sources / env. plan() (utils/hatch.rs:344) reads installer == "uv" for the env but never consults tool.uv.sources, so rewrite_project (utils/hatch.rs:187) rewrites a dependency that uv then resolves from the source. A fix could refuse (or warn and skip) any patched name that appears in [tool.uv.sources] when an env that installs the project uses the uv installer.
Related but different: #564 (uv lane, no-sources = true).
[agent] Found by the scheduled Hatch bug-hunt routine (ledger #314).
Summary
In a Hatch project whose environment uses
installer = "uv", a[tool.uv.sources]entry for the patched package (for example a url, path or git source) takes precedence over the PEP 508 direct reference that hosted mode writes into[project].dependencies. Hatch installs the project withuv pip install -e ., and uv applies the project'stool.uv.sources. Hostedscannever looks at[tool.uv.sources]. It rewritessix==1.16.0tosix @ <patch url>#sha256=…and reportssuccess,redirected: 1, exit 0. Then:urlorpathsource, every fresh Hatch env silently installs the unpatched upstream wheel;gitsource, every Hatch env creation fails withConflicting URLs for package six.The docs say a source table is refused before anything is written, so the scan should have failed closed.
Impact
CI gets a green scan and a pyproject that looks patched, but the vulnerable code still ships. For git sources the project stops installing at all.
vexdoes correctly omit six (not_applied) when it inspects the env, so the silent part is the scan result. Vendored mode on the same project refuses (withpypi_uv_no_lockfile), so it fails closed.Repro (Hatch 1.18.1, uv 0.12, Linux; local mock patch API serving a patched six 1.16.0 wheel)
Swap the source for
{ path = "/abs/six-1.16.0-py2.py3-none-any.whl" }to get the same result. With{ git = "https://github.com/benjaminp/six", tag = "1.16.0" },hatch runfails with:Expected vs actual
sourcesare, with nothing written and a non-zero or warning result.success,redirected: 1, no warning. The rewrite is either ignored (url/path) or breaks installs (git).Matrix (Linux, each cell run twice)
installer = "uv", project deps,tool.uv.sourcesurlinstaller = "uv", project deps,tool.uv.sourcespathinstaller = "uv", project deps,tool.uv.sourcesgitinstaller = "uv",features = ["x"](optional deps), url sourceinstaller = "uv", envdependencies, url sourceinstaller = "uv",tool.uv.sourcesindex source (explicit index)tool.uv.sources)macOS / Windows: not probed (probe branches can't be deleted from this sandbox right now). The rewrite is OS-independent.
Suspect code
crates/socket-patch-core/src/utils/hatch.rs:247refuses only Hatch's owntool.hatch.sources/env.plan()(utils/hatch.rs:344) readsinstaller == "uv"for the env but never consultstool.uv.sources, sorewrite_project(utils/hatch.rs:187) rewrites a dependency that uv then resolves from the source. A fix could refuse (or warn and skip) any patched name that appears in[tool.uv.sources]when an env that installs the project uses the uv installer.Related but different: #564 (uv lane,
no-sources = true).