Repository navigation
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
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 --checkreportsvendor_check_ok(exit 0)vexattestsnot_affected("Patched via Socket patch … (vendored)")- but
pipenv sync(orpip install -r requirements.txtin 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_existsix = {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@versionfrom a non-Socket source … the reference is then dropped with apatched_ref_unattributablediagnostic naming both files. This applies across … Pipfile.lock / requirements." AfileURL /pathwheel or aname @ urlline is a non-Socket source. The same mistake should makevendor --checkfail ("wiring contested"), as it does for a registry pin. - Actual: no diagnostic, check green,
not_affectedattested.
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_lockcallsresolved_elsewhereonly for an entry with no reference (a registry pin). An entry whose singlefile/pathreferenceclassifys toOk(None)(not Socket) reachesemit, which returns silently atpypi_other.rs:472(Ok(None) => return,) and records no evidence.pypi_other.rs:258:requirements_linedoes 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
pipenv: 2026-10-14 21:20Z run (#1549)
mikolalysenko commentedon Oct 14, 2026
[agent] Triaged as
priority:p1(Pipenv / PyPI family). Confirmed onmain015e871: incrates/socket-patch-core/src/vex/discover/pypi_other.rs,extract_pipfile_lock(line 174) andrequirements_line(line 261) callresolved_elsewhereonly for registry pins, andemitreturns 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
mikolalysenko commentedon Oct 15, 2026
[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
mikolalysenko commentedon Oct 15, 2026
[agent] Draft PR: #1562
Generated by Claude Code
Contest wiring with non-Socket Pipenv/pip refs
Test vex contest by Pipenv/pip direct refs
Contest wiring with editable and egg-named refs
Ignore user refs in an abandoned Pipfile.lock
Read legacy name-version #egg= fragments
Read -e editable values with no space