馃寪 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

馃悰 fix(create): reject ambiguous venv paths - #3426

Merged
gaborbernat merged 5 commits into
pypa:mainfrom
gaborbernat:security/venv-destination-lines
Oct 17, 2026
Merged

gaborbernat merged 5 commits into
pypa:mainfrom
gaborbernat:security/venv-destination-lines

Conversation

@gaborbernat

@gaborbernat gaborbernat commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

The stdlib venv creator (--creator venv) runs CPython's venv module to lay out the environment and then writes virtualenv's own pyvenv.cfg over the one venv left. venv records its command line in that file as the command key, destination included (create_configuration). pyvenv.cfg is a line-based format with no escaping, so a destination that contains a line boundary (any of the ten characters str.splitlines recognizes) ended the command line early and left the rest of the destination as a line of its own. virtualenv then read that file back and merged every line it parsed into the final configuration, so a destination such as env\nhome = /elsewhere set the environment's home, the key the interpreter reads at startup to find its base installation. 馃悰 After this PR the venv creator rejects a destination with a line boundary before it touches the filesystem, and the final pyvenv.cfg is built from the selected interpreter and virtualenv's own options alone. This addresses draft advisory ANT-2026-6ZQHJ6VI.

Destination text re-entered the configuration through the stdlib file

Venv.set_pyenv_cfg ran after venv had written its file. It called refresh, whose parser splits each line at its first =, let the base class rebuild virtualenv's own content, and laid the parsed stdlib values over it with update, under the comment "prefer venv options over ours". This order dates from the creator's first version. The text after a boundary in the destination therefore became a key and a value that replaced virtualenv's own, home included. write collapses line boundaries in each key and value, so the injected pair came out as a well-formed line of the final file.

set_pyenv_cfg no longer reads the stdlib file. The content comes from Creator.set_pyenv_cfg, which starts from an empty mapping and fills home, implementation, the version keys, executable, command, virtualenv and prompt from the selected interpreter and the options, and from ViaGlobalRefApi.set_pyenv_cfg, which adds include-system-site-packages. GraalPy's launchers read venvlauncher_command and base-executable from pyvenv.cfg; those two values used to arrive through the merge and are now set from the selected interpreter's system_exe.

flowchart LR
    V[venv writes pyvenv.cfg with command = ... dest] --> R[refresh parses every line at its first =]
    R --> U[update lays the parsed keys over virtualenv's own]
    U --> W[write collapses boundaries, injected line kept]
    S[Creator.set_pyenv_cfg from interpreter and options] --> G[GraalPy launcher keys from system_exe]
    G --> X[write]
    classDef bad fill:#ffd6d6,stroke:#c1121f,color:#5c0000;
    classDef ok fill:#d8f3dc,stroke:#2d6a4f,color:#1b4332;
    class V,R,U,W bad;
    class S,G,X ok;
Loading

Validation checked the path only after it had touched the filesystem

Creator.validate_dest checks that the filesystem codec can encode the destination, asks Path.exists whether a file sits there, resolves the path with Path.resolve, which follows symbolic links, refuses os.pathsep in the raw and the resolved value (the resolved check comes from #3399), and walks up to a writable parent. No check covered line boundaries: a file name holding one is legal on POSIX, and the venv creator was the only one that let it reach a line-based file unescaped.

Venv.validate_dest now runs the boundary check on the raw value before super().validate_dest and again on the resolved value it returns. The first check precedes every filesystem call, so a rejected destination leaves nothing behind. The second catches a symbolic link along the path whose target name holds a boundary. Both raise ArgumentTypeError, which argparse reports as a usage error with exit code 2. Other creators keep their destination support, since their configuration never passes through the stdlib file.

flowchart LR
    A[raw destination] --> B{line boundary?}
    B -- yes --> E1[ArgumentTypeError, no filesystem access]
    B -- no --> C[Creator.validate_dest: codec, exists, resolve, pathsep, writable parent]
    C --> D{boundary in the resolved path?}
    D -- yes --> E2[ArgumentTypeError]
    D -- no --> F[create]
    C -. before .-> O[create, venv writes the destination into pyvenv.cfg]
    classDef bad fill:#ffd6d6,stroke:#c1121f,color:#5c0000;
    classDef ok fill:#d8f3dc,stroke:#2d6a4f,color:#1b4332;
    class O bad;
    class E1,E2,F ok;
Loading

Behavior changes

A venv creator destination that contains any of the ten line-boundary characters fails with an argument error before virtualenv reads or creates anything. The final pyvenv.cfg holds virtualenv's keys only; keys the stdlib venv wrote, vendor-specific ones included, are no longer merged. On GraalPy, venvlauncher_command and base-executable are set to the selected interpreter's executable. The home value of macOS framework builds and Windows Store installs now comes from virtualenv's own computation instead of the stdlib file; this PR's tests do not cover those builds. Ordinary destination paths remain supported.

Reported by Anthropic's OSS Scanner: ANT-2026-6ZQHJ6VI.

Reported by Anthropic's OSS Scanner: ANT-2026-6ZQHJ6VI.
@gaborbernat gaborbernat added bug security Fixes a vulnerability or hardens the supply chain, CI or release labels Oct 16, 2026
Check line boundaries before pathname inspection, which raises an OS
error for invalid Windows names on older Python versions. Validate the
resolved destination as well before invoking stdlib venv.
@gaborbernat
gaborbernat marked this pull request as ready for review October 17, 2026 09:55
@gaborbernat
gaborbernat merged commit b0008a5 into pypa:main Oct 17, 2026
76 checks passed
@gaborbernat
gaborbernat deleted the security/venv-destination-lines branch October 17, 2026 10:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug security Fixes a vulnerability or hardens the supply chain, CI or release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant