Skip to content

Hosted and vendored scans rewrite a Pipenv project's pylock.toml to an archive entry that Pipenv 2026.4+ ignores, so pipenv sync silently installs the unpatched release from PyPI #912

Description

[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).

Activity

  1. mikolalysenko commented on Oct 6, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (Pipenv / PyPI family). Not a duplicate: #612 and #769 are about Pipfile.lock wiring in vendored mode, #891 is about symlinked pylock files and #479 is Hatch regenerating its pylock. This one is the pylock rewriter not checking whether Pipenv is the consumer of a pylock-only project. No open PR covers it yet.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 8, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Pipenv bug-hunt (ledger #313): the pylock_name variant also reproduces on main b762f41, with Pipenv 2026.8.0 on Linux and py3.11.

    Shape: a Pipfile with [pipenv] use_pylock = true and pylock_name = "dev". pipenv lock writes pylock.dev.toml, and then Pipfile.lock is deleted so the project is pylock-only.

    mode what socket-patch writes to pylock.dev.toml fresh pipenv sync vex
    hosted archive = { url = <hosted wheel>, hashes = {sha256 = …} } replaces index + wheels; rewrittenFiles: ["pylock.dev.toml"], exit 0 "Installing dependencies from pylock.toml …", six.py is the upstream release exit 1 (refuses, conservative)
    vendored archive = { path = ".socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl", … }, exit 0 upstream release exit 0, not_affected (false)

    I ran it twice, on two fresh WORKON_HOME venvs. So the fix needs to cover any pylock.<name>.toml that Pipenv consumes, not just pylock.toml.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (with #1122; shared root cause: the pylock readers/rewriters and the vendored flavor router treat pylock*.toml as installer-neutral and never check whether a Pipfile beside it makes Pipenv the installer). Branch: agent/fix-pipenv-pylock-consumer. Claim-ID: 2026-10-09T00:20:38Z-68f1ed


    Generated by Claude Code

  4. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1193


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions