Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
288 changes: 288 additions & 0 deletions SPECS/conda/CVE-2026-53940.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
From ff37b0c271c865bc5b812f046b210ecfd4008525 Mon Sep 17 00:00:00 2001
From: AllSpark <allspark@microsoft.com>
Date: Wed, 23 Sep 2026 05:19:05 +0000
Subject: [PATCH] Validate Python entry points and sanitize target paths

Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: AI Backport of https://github.com/conda/conda/pull/16168.patch
---
conda/common/path.py | 34 ++++++++++++++++++++++++++
conda/core/path_actions.py | 14 +++++++++--
conda/gateways/disk/create.py | 13 +++++++++-
news/validate-entry-points | 19 +++++++++++++++
tests/common/test_path.py | 35 +++++++++++++++++++++++++++
tests/core/test_path_actions.py | 42 +++++++++++++++++++++++++++++++++
6 files changed, 154 insertions(+), 3 deletions(-)
create mode 100644 news/validate-entry-points

diff --git a/conda/common/path.py b/conda/common/path.py
index a3fea08..e914fb5 100644
--- a/conda/common/path.py
+++ b/conda/common/path.py
@@ -3,6 +3,7 @@
"""Common path utilities."""
from __future__ import annotations

+import keyword
import os
import re
import subprocess
@@ -138,6 +139,19 @@ def explode_directories(child_directories: Iterable[tuple[str, ...]]) -> set[str
)


+def is_valid_import_path(path: str) -> bool:
+ """
+ Python import paths are a sequence of non-keyword Python identifiers separated by one period.
+ """
+ # Empty strings are not valid, and return an empty list on split(), making all([]) truthy!
+ if not path:
+ return False
+
+ return all(
+ part.isidentifier() and not keyword.iskeyword(part) for part in path.split(".")
+ )
+
+
def pyc_path(py_path, python_major_minor_version):
"""
This must not return backslashes on Windows as that will break
@@ -171,6 +185,26 @@ 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()
+
+ # Validate command
+ if not command or any(c in command for c in ("/", "\\", "\0", "\n", "\r")):
+ raise ValueError(
+ "entry point command must be a simple file name; "
+ f"got invalid characters in {command!r}"
+ )
+ if command.startswith("."):
+ raise ValueError(
+ "entry point command must not be a path-traversal token "
+ f"or hidden name: {command!r}"
+ )
+ # Validate module and func
+ if not is_valid_import_path(module):
+ raise ValueError(
+ f"'{module}' is not a valid absolute import of a Python module"
+ )
+ if not is_valid_import_path(func):
+ raise ValueError(f"'{func}' is not a valid Python function identifier")
+
return command, module, func


diff --git a/conda/core/path_actions.py b/conda/core/path_actions.py
index 2b87d0b..acd8caa 100644
--- a/conda/core/path_actions.py
+++ b/conda/core/path_actions.py
@@ -1,13 +1,14 @@
# Copyright (C) 2012 Anaconda, Inc
# SPDX-License-Identifier: BSD-3-Clause
"""Atomic actions that make up a package installation or removal transaction."""
+import os
import re
import sys
from abc import ABCMeta, abstractmethod, abstractproperty
from itertools import chain
from json import JSONDecodeError
from logging import getLogger
-from os.path import basename, dirname, getsize, isdir, join
+from os.path import basename, dirname, getsize, isdir, join, normpath
from uuid import uuid4

from .. import CondaError
@@ -304,6 +305,10 @@ class LinkPathAction(CreateInPrefixPathAction):
source_short_path = "Scripts/conda.exe"
command, _, _ = parse_entry_point_def(entry_point_def)
target_short_path = "Scripts/%s.exe" % command
+ if not normpath(target_short_path).startswith("Scripts" + os.sep):
+ raise ValueError(
+ "target_short_path must point to Scripts/: " + target_short_path
+ )
source_path_data = PathDataV1(
_path=target_short_path,
path_type=PathType.windows_python_entry_point_exe,
@@ -774,7 +779,12 @@ class CreatePythonEntryPointAction(CreateInPrefixPathAction):

def this_triplet(entry_point_def):
command, module, func = parse_entry_point_def(entry_point_def)
- target_short_path = f"{get_bin_directory_short_path()}/{command}"
+ bin_directory = get_bin_directory_short_path()
+ target_short_path = f"{bin_directory}/{command}"
+ if not normpath(target_short_path).startswith(bin_directory + os.sep):
+ raise ValueError(
+ f"target_short_path must point to {bin_directory}: {target_short_path!r}"
+ )
if on_win:
target_short_path += "-script.py"
return target_short_path, module, func
diff --git a/conda/gateways/disk/create.py b/conda/gateways/disk/create.py
index 9aa7002..25b3be2 100644
--- a/conda/gateways/disk/create.py
+++ b/conda/gateways/disk/create.py
@@ -16,7 +16,13 @@ from ...auxlib.ish import dals
from ...base.constants import CONDA_PACKAGE_EXTENSION_V1, PACKAGE_CACHE_MAGIC_FILE
from ...base.context import context
from ...common.compat import on_linux, on_win
-from ...common.path import ensure_pad, expand, win_path_double_escape, win_path_ok
+from ...common.path import (
+ ensure_pad,
+ expand,
+ is_valid_import_path,
+ win_path_double_escape,
+ win_path_ok,
+)
from ...common.serialize import json_dump
from ...exceptions import BasicClobberError, CondaOSError, maybe_raise
from ...models.enums import LinkType
@@ -116,6 +122,11 @@ def write_as_json_to_file(file_path, obj):


def create_python_entry_point(target_full_path, python_full_path, module, func):
+ if not is_valid_import_path(module):
+ raise ValueError("'module' is not a valid Python import path")
+ if not is_valid_import_path(func):
+ raise ValueError("'func' is not a valid Python import path")
+
if lexists(target_full_path):
maybe_raise(
BasicClobberError(
diff --git a/news/validate-entry-points b/news/validate-entry-points
new file mode 100644
index 0000000..1d1689c
--- /dev/null
+++ b/news/validate-entry-points
@@ -0,0 +1,19 @@
+### Enhancements
+
+* <news item>
+
+### Bug fixes
+
+* Validate Python entry point definitions before generating entry point scripts. (#16168)
+
+### Deprecations
+
+* <news item>
+
+### Docs
+
+* <news item>
+
+### Other
+
+* <news item>
diff --git a/tests/common/test_path.py b/tests/common/test_path.py
index 61edfb1..c7cd81d 100644
--- a/tests/common/test_path.py
+++ b/tests/common/test_path.py
@@ -2,8 +2,11 @@
# SPDX-License-Identifier: BSD-3-Clause
from logging import getLogger

+import pytest
+
from conda.common.path import (
get_major_minor_version,
+ is_valid_import_path,
missing_pyc_files,
url_to_path,
win_path_backout,
@@ -168,3 +171,35 @@ def test_get_major_minor_version_no_dot():
assert get_major_minor_version("bin/python3.10", False) == "310"
assert get_major_minor_version("lib/python310/site-packages/", False) == "310"
assert get_major_minor_version("python3", False) is None
+
+
+@pytest.mark.parametrize(
+ "path,result",
+ [
+ ("python", True),
+ ("python.path", True),
+ ("python._path0123", True),
+ # Keywords are forbidden
+ ("import", False),
+ ("import.path", False),
+ # Numbers cannot start the name
+ ("0mod.import", False),
+ # Empty strings or components are not valid
+ ("", False),
+ (".", False),
+ ("..", False),
+ # This also applies to relative imports
+ (".base.common", False),
+ ("..parent.base.common", False),
+ # Some non-ASCII characters are ok!
+ ("ñándú", True),
+ ("ñ.α", True),
+ # Emojis are not
+ ("🚨", False),
+ ("a🚨", False),
+ # Injection prevented
+ ('something"); malicious()', False),
+ ],
+)
+def test_is_valid_import_path(path, result):
+ assert is_valid_import_path(path) is result
diff --git a/tests/core/test_path_actions.py b/tests/core/test_path_actions.py
index ecd1ce8..e29bbc4 100644
--- a/tests/core/test_path_actions.py
+++ b/tests/core/test_path_actions.py
@@ -1,5 +1,7 @@
# Copyright (C) 2012 Anaconda, Inc
# SPDX-License-Identifier: BSD-3-Clause
+from __future__ import annotations
+
import importlib.util
import os
import sys
@@ -293,6 +295,46 @@ def test_CreatePythonEntryPointAction_noarch_python(prefix: Path):
assert not isfile(windows_exe_axn.target_full_path)


+@pytest.mark.parametrize(
+ "definition,result_or_exc",
+ [
+ ("command1=some.module:main", ("command1", "some.module", "main")),
+ (
+ "command1=some.module:SomeClass.method",
+ ("command1", "some.module", "SomeClass.method"),
+ ),
+ (
+ "../bin/python=some.module:main",
+ (ValueError, "simple file name"),
+ ),
+ (
+ "command=.some.module:main",
+ (ValueError, "not a valid absolute import of a Python module"),
+ ),
+ (
+ "command=some..module:main",
+ (ValueError, "not a valid absolute import of a Python module"),
+ ),
+ (
+ "command=some.module:main-function",
+ (ValueError, "not a valid Python function identifier"),
+ ),
+ (
+ "command=some.module:main..function",
+ (ValueError, "not a valid Python function identifier"),
+ ),
+ ],
+)
+def test_entry_point_parse_def(
+ definition: str, result_or_exc: tuple[str, str, str] | tuple[Exception, str]
+):
+ if isinstance(result_or_exc[0], str):
+ assert parse_entry_point_def(definition) == result_or_exc
+ else:
+ with pytest.raises(result_or_exc[0], match=result_or_exc[1]):
+ parse_entry_point_def(definition)
+
+
def test_simple_LinkPathAction_hardlink(prefix: Path, pkgs_dir: Path):
source_full_path = make_test_file(pkgs_dir)
target_short_path = source_short_path = basename(source_full_path)
6 changes: 5 additions & 1 deletion SPECS/conda/conda.spec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Summary: Cross-platform, Python-agnostic binary package manager
Name: conda
Version: 24.3.0
Release: 4%{?dist}
Release: 5%{?dist}
License: BSD-3-Clause AND Apache-2.0
# The conda code is BSD-3-Clause
# adapters/ftp.py is Apache-2.0
Expand All @@ -20,6 +20,7 @@ Patch3: conda-cpuinfo.patch
Patch10004: 0004-Do-not-try-to-run-usr-bin-python.patch
Patch10005: 0005-Fix-failing-tests-in-test_api.py.patch
Patch10006: 0006-shell-assume-shell-plugins-are-in-etc.patch
Patch10007: CVE-2026-53940.patch

BuildArch: noarch

Expand Down Expand Up @@ -402,6 +403,9 @@ PYTHONPATH=%{buildroot}%{python3_sitelib} conda info
%{_datadir}/conda/condarc.d/

%changelog
* Wed Sep 23 2026 Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> - 24.3.0-5
- Patch for CVE-2026-53940

* Thu Aug 07 2025 Riken Maharjan <rmaharjan@microsoft.com> - 24.3.0-4
- Add missing conda.xsh file to /etc/profile.d
- also move conda.fish to /etc/fish/conf.d/
Expand Down
Loading