Skip to content

Poetry with virtualenvs.in-project = true but no ./.venv keeps using its out-of-tree env, which socket-patch never probes: agent patches the global interpreter and hosted VEX falsely attests #476

Description

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

Summary

With virtualenvs.in-project = true set and no ./.venv directory, Poetry keeps installing into the project's existing out-of-tree env (~/.cache/pypoetry/virtualenvs/<name>-<hash>-py3.X). That is what happens when a user turns on poetry config virtualenvs.in-project true (global or --local) in a project that already has a Poetry env. poetry env info -p and poetry install both keep using the old env until it is removed.

socket-patch reads an explicit in-project = true as "only ./.venv" and never probes Poetry's out-of-tree env. So:

  • Agent mode (scan --mode agent) falls back to the global interpreter. If that interpreter has the same release (here a pip install --user six==1.16.0), it patches the global copy and reports added, exit 0. Otherwise it reports skipped / package_not_installed, exit 0. Either way, the env Poetry installed into stays unpatched.
  • Hosted mode (scan --mode hosted --vex): the v5 installed-bytes check (redirect_pypi_stale_install) never runs because no project venv is found. The same-run VEX then attests not_affected / inline_mitigations_already_exist while Poetry's env still imports upstream six.

This is the mirror image of #327 case 3 (in-project = false + stray .venv), which #330 fixed. #330 kept this side: uses_in_project_venv returns the explicit setting without checking for ./.venv, and poetry_virtualenvs_root returns None for in_project == Some(true).

Impact

  • A false VEX attestation in the default (hosted) mode. A consumer is told the vulnerability is mitigated while the running code is unpatched.
  • Agent mode can write to the user's global site-packages, which is outside the project, while reporting success.
  • This is a common configuration. Setting virtualenvs.in-project true globally is a popular tweak, and Poetry silently keeps using the pre-existing cache env, so users don't notice that .venv was never created.

Repro (Linux, Poetry 2.5.1, Python 3.11)

The mock patch API serves one patch for pkg:pypi/six@1.16.0 that appends SOCKET_PATCHED = 1 to six.py. It uses the same routes as tests/vex_pypi_real_common/mod.rs (batch, view, the /patches/package grant and the wheel) plus /patches/blob/<hash>. $A = --api-url http://127.0.0.1:18766 --api-token fake --org test-org --patch-server-url http://127.0.0.1:18766.

mkdir ipapp && cd ipapp
cat > pyproject.toml <<'TOML'
[tool.poetry]
name = "ipapp"
version = "0.1.0"
description = ""
authors = ["x <x@x>"]
[tool.poetry.dependencies]
python = "^3.9"
six = "1.16.0"
TOML
poetry install --no-root                       # default config: ~/.cache/pypoetry/virtualenvs/ipapp-<hash>-py3.11
poetry config virtualenvs.in-project true      # (or --local; same result)
poetry install --no-root                       # Poetry keeps using the existing out-of-tree env
poetry env info -p                             # -> ~/.cache/pypoetry/virtualenvs/ipapp-<hash>-py3.11 ; no ./.venv
E=$(poetry env info -p)

# Agent mode
socket-patch scan --mode agent --yes --json --ecosystems pypi $A   # scannedPackages 105 (global interpreter), action "added", exit 0
tail -1 $E/lib/python3.11/site-packages/six.py                     # upstream line: NOT patched
tail -1 ~/.local/lib/python3.11/site-packages/six.py               # SOCKET_PATCHED = 1  <- the global copy got patched
# (with no global six: action "skipped", errorCode "package_not_installed", exit 0)

# Hosted mode (restore everything first; the env still holds upstream six)
socket-patch scan --mode hosted --yes --json --ecosystems pypi --vex vex.json --vex-product pkg:pypi/ipapp@0.1.0 $A
# -> redirected: 1, no redirect_pypi_stale_install warning, exit 0
jq '.statements[] | [.status, .justification]' vex.json   # ["not_affected","inline_mitigations_already_exist"]
$E/bin/python -c 'import six; print(hasattr(six, "SOCKET_PATCHED"))'   # False

Control: the same project without in-project = true (or with poetry.toml removed) gets added in Poetry's env, and hosted emits redirect_pypi_stale_install / vex_omitted for the stale env.

Expected vs actual

  • Expected: docs/testing/poetry-compatibility.md ("Mode notes") says the crawler probes "the virtualenv(s) Poetry placed under its virtualenvs.path" for "a Poetry project whose ./.venv Poetry would not use (an explicit virtualenvs.in-project = false, or no ./.venv at all)". The same section says hosted's stale check "excludes the package from same-run VEX". Poetry's own EnvManager.get() returns ./.venv only if it exists (in_project_venv_exists()), and otherwise returns <virtualenvs.path>/<name>-<hash>-py<minor> whenever that exists, whatever in-project says.
  • Actual: with in-project = true and no ./.venv, the Poetry env is never probed. Agent mode patches the wrong interpreter or skips with exit 0, and hosted VEX attests from the lock pin alone.

OS × version

OS Poetry in-project = true in Agent: Poetry env Agent: global user-site six Hosted --vex with a stale env
Linux 1.1.15 (py3.9) poetry.toml ❌ unpatched ❌ patched instead ❌ not_affected (plus the redirect_poetry_stale_install_risk advisory)
Linux 1.8.5 poetry.toml ❌ ❌ ❌ not_affected
Linux 2.0.1 poetry.toml ❌ ❌ ❌ not_affected
Linux 2.5.1 poetry.toml (2/2) ❌ ❌ ❌ not_affected
Linux 2.5.1 user config.toml (2/2) ❌ ❌ not run
macOS / Windows — — not run: this routine can't use probe branches this run

The logic is platform-independent (config precedence only).

First bad version

It never worked. Release 4.0.0 has no Poetry out-of-tree discovery. #241 added it and #330 (5678b76) reworked it, and both skip the out-of-tree root when in_project == Some(true). Main 6e7ef74 reproduces.

Suspect code

  • crates/socket-patch-core/src/crawlers/python_crawler.rs:634 PoetryProject::uses_in_project_venv: returns an explicit in_project as-is. Poetry's get() only takes ./.venv when the directory exists (in_project_venv_exists), and otherwise falls through to the out-of-tree env.
  • crates/socket-patch-core/src/crawlers/python_crawler.rs:571 poetry_virtualenvs_root: config.in_project == Some(true) returns None, so even a direct call can't find the env.
  • crates/socket-patch-cli/src/commands/scan/hosted/python.rs:40-56: the stale-install check relies on find_local_venv_site_packages, so it silently has no evidence and VEX attests.

Related

Activity

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