[agent] Found by the scheduled Pipenv bug-hunt routine (ledger #313).
Summary
Pipenv 2026 supports PEP 751 lock files. With [pipenv] use_pylock = true in the Pipfile, pipenv lock writes pylock.toml next to Pipfile.lock. When Pipfile.lock is absent (a pylock-only checkout, or a pylock.toml from another tool next to a Pipfile), pipenv sync installs from pylock.toml (Lockfile.content / get_or_create_lockfile's pylock branch).
scan --mode hosted and scan --mode vendored rewrite that pylock.toml the PEP 751 way: they replace index and wheels with archive = { url = "<hosted wheel>" … } (hosted) or archive = { path = ".socket/vendor/pypi/<uuid>/<wheel>" … } (vendored), and keep version = "1.16.0". Pipenv reads pylock.toml through pipenv/utils/pylock.py PylockFile.convert_to_pipenv_lockfile, which copies only version, marker and the wheels / sdist hashes. It ignores archive. So the patched entry becomes six = {version = "==1.16.0"} with no hashes and no URL, and pipenv sync installs the upstream release from PyPI, exit 0.
The scan reports status: success, redirected: 1, rewrittenFiles: ["pylock.toml"] and gives no warning.
Impact
- A Pipenv 2026.4+ project that installs from
pylock.toml thinks it is patched, but every fresh pipenv sync (CI, Docker) installs the vulnerable bytes without a hash check.
- Vendored:
socket-patch vex then attests not_affected / inline_mitigations_already_exist for the package, even though the venv pipenv sync just built holds the upstream six.py. That is a false VEX statement. In hosted mode, vex refuses (no_applicable_patches), which is correct.
- When
Pipfile.lock is also present (the normal use_pylock = true layout), Pipenv installs from Pipfile.lock and the patch lands (pass, see the table below). Only the pylock-only shape is affected.
Repro (Linux, real Pipenv 2026.8.0, local mock patch API serving a patched six 1.16.0 wheel)
mkdir demo && cd demo
cat > Pipfile <<'EOF'
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
six = "==1.16.0"
[pipenv]
use_pylock = true
EOF
pipenv lock && rm Pipfile.lock # pylock-only checkout
socket-patch scan --mode hosted --yes --json --ecosystems pypi \
--api-url $MOCK --org test-org --api-token x --patch-server-url $MOCK
# -> status success, redirected 1, rewrittenFiles ["pylock.toml"], no warnings
grep archive pylock.toml # archive = { url = "$MOCK/patch/pypi/six/1.16.0/<grant>/<uuid>/six-1.16.0-py2.py3-none-any.whl", hashes = {...} }
pipenv sync; echo $? # 0
pipenv run python -c "import six; print(getattr(six, 'SOCKET_PATCHED', False))" # False: the upstream six
Vendored is the same with --mode vendored: archive = { path = ".socket/vendor/pypi/<uuid>/<wheel>" … }, then pipenv sync gives the upstream six, and socket-patch vex --product pkg:pypi/demo@1.0.0 --output vex.json attests not_affected.
Reproduced 2/2 for hosted on 2026.8.0, and 2/2 for vendored on 2026.8.0.
Expected vs actual
- Expected: docs/ecosystems.md lists PEP 751
pylock.toml among the hosted / vendored lock formats, and CLI_CONTRACT.md says a dep counts as redirected only when the reference "actually landed in a project file". The rewrite has to be one the project's installer honours. A rewrite that leaves the next frozen install on the unpatched bytes is a defect. When Pipenv is the consumer of pylock.toml (a Pipfile beside it and no Pipfile.lock), socket-patch should either write a shape Pipenv's converter keeps, or refuse / warn (e.g. a redirect_pipenv_* code telling the user to commit a Pipfile.lock) and not count it as redirected or attest it.
- Actual: success, no warning, and an unpatched install. In vendored mode, a false
not_affected VEX.
OS × version
| Pipenv |
pipenv sync after hosted rewrite of a pylock-only project |
Notes |
| 2025.1.3 |
n/a |
no pylock support; scan rewrites nothing (pass) |
| 2026.0.0, 2026.1.0, 2026.2.0, 2026.2.2 |
exit 1 Pipfile.lock not found! |
loud failure, not silent |
| 2026.4.0, 2026.6.0, 2026.7.1, 2026.8.0 |
exit 0, upstream six installed |
fail |
2026.8.0, Pipfile.lock + pylock.toml both present |
PATCHED (sync, install --deploy, verify, requirements) |
pass |
Linux only. macOS / Windows probe branches could not be used this run (see ledger). The Pipenv code is pure Python, so the behaviour should be the same on every OS.
First bad Pipenv release: 2026.4.0 (the first release whose sync installs from a pylock-only project). socket-patch: current main 9c43dfc. I did not bisect socket-patch, because the pylock rewriter has never written a shape that Pipenv reads.
Suspect code
- The pylock rewriters and vendoring treat
pylock*.toml as consumer-neutral (crates/socket-patch-core/src/utils/python_lock.rs:177 is_python_lock_name, the hosted candidate list at crates/socket-patch-core/src/hosted/engine.rs:372, vendoring in crates/socket-patch-core/src/vendor/pypi_lock.rs). Nothing checks whether a Pipfile beside the lock makes Pipenv the installer.
- Pipenv side, for reference:
pipenv/utils/pylock.py PylockFile.convert_to_pipenv_lockfile (2026.8.0) reads only wheels[*].hashes.sha256 and sdist.hashes.sha256.
Related but distinct: #479 (Hatch regenerates its pylock.toml).
[agent] Found by the scheduled Pipenv bug-hunt routine (ledger #313).
Summary
Pipenv 2026 supports PEP 751 lock files. With
[pipenv] use_pylock = truein the Pipfile,pipenv lockwritespylock.tomlnext toPipfile.lock. WhenPipfile.lockis absent (a pylock-only checkout, or apylock.tomlfrom another tool next to a Pipfile),pipenv syncinstalls frompylock.toml(Lockfile.content/get_or_create_lockfile's pylock branch).scan --mode hostedandscan --mode vendoredrewrite thatpylock.tomlthe PEP 751 way: they replaceindexandwheelswitharchive = { url = "<hosted wheel>" … }(hosted) orarchive = { path = ".socket/vendor/pypi/<uuid>/<wheel>" … }(vendored), and keepversion = "1.16.0". Pipenv readspylock.tomlthroughpipenv/utils/pylock.pyPylockFile.convert_to_pipenv_lockfile, which copies onlyversion,markerand thewheels/sdisthashes. It ignoresarchive. So the patched entry becomessix = {version = "==1.16.0"}with no hashes and no URL, andpipenv syncinstalls the upstream release from PyPI, exit 0.The scan reports
status: success,redirected: 1,rewrittenFiles: ["pylock.toml"]and gives no warning.Impact
pylock.tomlthinks it is patched, but every freshpipenv sync(CI, Docker) installs the vulnerable bytes without a hash check.socket-patch vexthen attestsnot_affected/inline_mitigations_already_existfor the package, even though the venvpipenv syncjust built holds the upstreamsix.py. That is a false VEX statement. In hosted mode, vex refuses (no_applicable_patches), which is correct.Pipfile.lockis also present (the normaluse_pylock = truelayout), Pipenv installs fromPipfile.lockand the patch lands (pass, see the table below). Only the pylock-only shape is affected.Repro (Linux, real Pipenv 2026.8.0, local mock patch API serving a patched six 1.16.0 wheel)
Vendored is the same with
--mode vendored:archive = { path = ".socket/vendor/pypi/<uuid>/<wheel>" … }, thenpipenv syncgives the upstream six, andsocket-patch vex --product pkg:pypi/demo@1.0.0 --output vex.jsonattestsnot_affected.Reproduced 2/2 for hosted on 2026.8.0, and 2/2 for vendored on 2026.8.0.
Expected vs actual
pylock.tomlamong the hosted / vendored lock formats, and CLI_CONTRACT.md says a dep counts as redirected only when the reference "actually landed in a project file". The rewrite has to be one the project's installer honours. A rewrite that leaves the next frozen install on the unpatched bytes is a defect. When Pipenv is the consumer ofpylock.toml(a Pipfile beside it and noPipfile.lock), socket-patch should either write a shape Pipenv's converter keeps, or refuse / warn (e.g. aredirect_pipenv_*code telling the user to commit aPipfile.lock) and not count it as redirected or attest it.not_affectedVEX.OS × version
pipenv syncafter hosted rewrite of a pylock-only projectPipfile.lock not found!Pipfile.lock+pylock.tomlboth presentsync,install --deploy,verify,requirements)Linux only. macOS / Windows probe branches could not be used this run (see ledger). The Pipenv code is pure Python, so the behaviour should be the same on every OS.
First bad Pipenv release: 2026.4.0 (the first release whose
syncinstalls from a pylock-only project). socket-patch: current main9c43dfc. I did not bisect socket-patch, because the pylock rewriter has never written a shape that Pipenv reads.Suspect code
pylock*.tomlas consumer-neutral (crates/socket-patch-core/src/utils/python_lock.rs:177is_python_lock_name, the hosted candidate list atcrates/socket-patch-core/src/hosted/engine.rs:372, vendoring incrates/socket-patch-core/src/vendor/pypi_lock.rs). Nothing checks whether a Pipfile beside the lock makes Pipenv the installer.pipenv/utils/pylock.pyPylockFile.convert_to_pipenv_lockfile(2026.8.0) reads onlywheels[*].hashes.sha256andsdist.hashes.sha256.Related but distinct: #479 (Hatch regenerates its pylock.toml).