#Summary
CVE-2026-53940 is a path traversal vulnerability in Conda versions prior to 26.5.2 that allows arbitrary file write with code execution. A malicious noarch:python package can write executable files outside the installation prefix or overwrite existing entry points like bin/pip. When an attacker-controlled entry point is invoked, it runs arbitrary Python code with the installing user's privileges - typically root in containerized environments. CVSS score: 8.8 HIGH.
#Am I affected?
- Affected: Conda
< 26.5.2 - Patched: Conda
>= 26.5.2 - Default configuration: affected (default
path_conflict: clobbersilently overwrites files) - Access needed: installation of a malicious package from any channel (file path, HTTP, conda-forge, etc.)
#How to check
Check your conda version:
conda --version| Output | Verdict |
|---|---|
conda 26.5.1 or earlier |
Vulnerable |
conda 26.5.2 or later |
Patched |
If the output is unclear, the real verification is whether the entry-point parser validates input. Run this in your conda base environment:
python -c "import conda.common.path.python as p; import inspect; print('PATCHED' if 'ValueError' in inspect.getsource(p.parse_entry_point_def) or hasattr(p, 'is_valid_import_path') else 'VULNERABLE')"#Fix and mitigation
- Fix: Upgrade conda to version 26.5.2 or later.
- If you cannot upgrade: The default configuration can be hardened by setting
path_conflict: preventin.condarc, which will reject any package that conflicts with existing files. However, this does not prevent out-of-prefix writes to directories that already exist. - Detection: If you run conda install and see a
ValueErrormentioning "entry point command must be a simple file name" or "target_short_path must point to", the patch is in place.
#Root cause analysis
#Vulnerable code path
The vulnerability spans four stages in conda's entry-point handling. A noarch:python package declares console scripts in its metadata file info/link.json:
{
"package_metadata_version": 1,
"noarch": {"type": "python", "entry_points": ["mytool = mypkg.cli:main"]}
}This metadata is user-controlled. Unlike files inside the package payload, it is never extracted or validated.
Step 1: The parser does no validation. The function parse_entry_point_def in conda/common/path/python.py (lines 40-44, v26.5.1) is the entire parsing layer:
def parse_entry_point_def(ep_definition):
cmd_mod, func = ep_definition.rsplit(":", 1)
command, module = cmd_mod.rsplit("=", 1)
command, module, func = command.strip(), module.strip(), func.strip()
return command, module, funcThe command component may contain path separators, traversal segments (..), NUL bytes, or newlines - none are checked.
Step 2: The command is embedded in a path. The function CreatePythonEntryPointAction.create_actions in conda/core/path_actions.py (lines 824-863) builds:
target_short_path = f"{BIN_DIRECTORY}/{command}"where BIN_DIRECTORY is "bin" on Unix or "Scripts" on Windows. This is a raw f-string concatenation with no path normalization.
Step 3: The join preserves traversal segments. The function PrefixPathAction.target_full_path (lines 193-209) calls:
return join(trgt, win_path_ok(shrt_pth))os.path.join preserves .. segments literally. They are only resolved by the kernel at open() time, after every conda check has passed. The action's verify() method is a no-op.
Step 4: The file is written with execute permission. The function create_python_entry_point in conda/gateways/disk/create.py (lines 130-162) writes the file:
with open(target_full_path, mode="w", encoding="utf-8") as fo:
fo.write(pyscript)
...
make_executable(target_full_path)The lexists guard before this is not a security control. The shipped default path_conflict: clobber overwrites existing files silently with no warning.
#Secondary primitive: template injection
The module and func fields from the entry-point string are interpolated verbatim into the generated wrapper script:
python_entry_point_template = """
from %(module)s import %(import_name)s
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?
