Skip to content

Vendored PyPI wiring isn't treated as contested when Pipfile.lock (or requirements.txt) points the same release at a non-Socket file/URL/path, so vendor --check stays green and vex attests not_affected while pipenv sync installs upstream #1549

Closed

Description

[agent] Found by the scheduled Pipenv bug-hunt routine (ledger #313).

Summary

In a Pipenv project with two install sources (Pipfile.lock plus a root requirements.txt), lockfile discovery counts the other lock as contesting a Socket reference only when it holds a registry pin. If the other lock resolves the same name@version from a non-Socket direct reference (a Pipfile.lock file URL or path entry, or a requirements.txt six @ https://…whl line), discovery ignores it. As a result:

  • vendor --check reports vendor_check_ok (exit 0)
  • vex attests not_affected ("Patched via Socket patch … (vendored)")
  • but pipenv sync (or pip install -r requirements.txt in the mirror case) installs the unpatched upstream wheel.

With a plain registry pin (six = "==1.16.0") in the same place, the same commands correctly go red (wiring contested) and vex withholds the statement. That's the contract's behaviour.

Impact

This is a false VEX attestation. A compliance pipeline gating on vendor --check + vex passes a build whose Pipenv install ships the vulnerable release. Realistic triggers: moving a pip project (vendored requirements.txt) to Pipenv with a URL/local-wheel pin in the Pipfile, or pipenv install ./wheels/six-…whl / pipenv install "six @ https://…" after vendoring. In the mirror case, someone adds a direct-URL requirement to a vendored Pipenv project's requirements.txt.

Repro (Pipenv 2026.9.1, Linux; mock patch API serving a patched six 1.16.0 wheel)

mkdir app && cd app
printf 'six==1.16.0\n' > requirements.txt
socket-patch scan --mode vendored --yes            # requirements.txt -> ./.socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl
URL=https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl
cat > Pipfile <<EOF
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
six = {file = "$URL"}
EOF
pipenv lock && pipenv sync
pipenv run python -c "import six; print(getattr(six,'SOCKET_PATCHED',0))"   # 0 -> UPSTREAM
socket-patch vendor --check --json      # status success, vendor_check_ok, exit 0
socket-patch vex --product pkg:generic/app@1   # "status": "not_affected", inline_mitigations_already_exist

six = {path = "./wheels/six-1.16.0-py2.py3-none-any.whl"} (the upstream wheel copied locally) behaves the same.

Mirror case: vendor a Pipenv project (Pipfile.lock wired), then printf 'six @ %s\n' "$URL" > requirements.txt. pip install -r requirements.txt gives UPSTREAM, while check is green and vex says not_affected. With six==1.16.0 there instead, check fails and vex withholds.

Expected vs actual

  • Expected (crates/socket-patch-cli/CLI_CONTRACT.md:440, Contested locks): "When one lock wires a package to a patch and another lock resolves the same name@version from a non-Socket source … the reference is then dropped with a patched_ref_unattributable diagnostic naming both files. This applies across … Pipfile.lock / requirements." A file URL / path wheel or a name @ url line is a non-Socket source. The same mistake should make vendor --check fail ("wiring contested"), as it does for a registry pin.
  • Actual: no diagnostic, check green, not_affected attested.

Matrix (Linux, main 015e871; 2 runs per cell)

Pipenv other source in Pipfile.lock sync vendor --check vex
2022.12.19 file URL UPSTREAM ok (wrong) not_affected (wrong)
2022.12.19 path wheel UPSTREAM ok (wrong) not_affected (wrong)
2023.12.1 file URL UPSTREAM ok (wrong) not_affected (wrong)
2023.12.1 path wheel UPSTREAM ok (wrong) not_affected (wrong)
2026.9.1 file URL UPSTREAM ok (wrong) not_affected (wrong)
2026.9.1 path wheel UPSTREAM ok (wrong) not_affected (wrong)
2022.12.19 / 2026.9.1 registry ==1.16.0 (control) UPSTREAM failed: contested ✔ withheld ✔
2026.9.1 mirror: requirements.txt six @ URL pip: UPSTREAM ok (wrong) not_affected (wrong)

macOS / Windows not probed (discovery is OS-independent).

Suspect code

  • crates/socket-patch-core/src/vex/discover/pypi_other.rs:171: extract_pipfile_lock calls resolved_elsewhere only for an entry with no reference (a registry pin). An entry whose single file/path reference classifys to Ok(None) (not Socket) reaches emit, which returns silently at pypi_other.rs:472 (Ok(None) => return,) and records no evidence.
  • pypi_other.rs:258: requirements_line does the same for a direct-reference line (requirement_location → emit → Ok(None) => return).
  • For comparison, the poetry.lock extractor treats a non-Socket source URL as evidence (crates/socket-patch-core/src/vex/discover/pypi_locks.rs:819-821: Ok(None) => { out.resolved_elsewhere(file, package_purl(package)); … }).

The Pipfile.lock file entry carries no version key, so the purl has to come from the wheel filename (or version when present). For a non-wheel reference that can't be versioned, the fail-closed choice would be to contest any reference of the same package name.

Not bisected: the contest rule and this extractor shape go back to the v5 discovery rewrite.

Activity

  1. mikolalysenko commented on Oct 14, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (Pipenv / PyPI family). Confirmed on main 015e871: in crates/socket-patch-core/src/vex/discover/pypi_other.rs, extract_pipfile_lock (line 174) and requirements_line (line 261) call resolved_elsewhere only for registry pins, and emit returns silently on a non-Socket reference (line 472, Ok(None) => return). The poetry.lock extractor already records such references as contesting evidence. No open or merged PR addresses this yet; no duplicate found.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: Pipfile.lock / requirements.txt discovery records a non-Socket direct reference as no evidence instead of contesting evidence). Branch: agent/fix-pypi-direct-ref-contest. Claim-ID: 2026-10-15T04:21:25Z-0505f0


    Generated by Claude Code

  3. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1562


    Generated by Claude Code

  4. added 6 commits that reference this issue on Oct 15, 2026
    3763ea3
    39605f6
    37e7c12
    e97bceb
    543e124
    07503e7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions