VYPR
High severityNVD Advisory· Published Jan 11, 2024· Updated Sep 3, 2024

Untrusted search path under some conditions on Windows allows arbitrary code execution

CVE-2024-22190

Description

GitPython is a python library used to interact with Git repositories. There is an incomplete fix for CVE-2023-40590. On Windows, GitPython uses an untrusted search path if it uses a shell to run git, as well as when it runs bash.exe to interpret hooks. If either of those features are used on Windows, a malicious git.exe or bash.exe may be run from an untrusted repository. This issue has been patched in version 3.1.41.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

GitPython on Windows has an incomplete fix for CVE-2023-40590, allowing untrusted search path to run malicious git.exe or bash.exe if shell or hooks are used.

CVE-2024-22190 is an incomplete fix for CVE-2023-40590 in GitPython, a Python library for Git operations. On Windows, when GitPython uses a shell to run git or executes bash.exe to interpret hooks, it relies on an untrusted search path. This allows a malicious git.exe or bash.exe from an untrusted repository to be executed instead of the legitimate system binary [1][3].

The attack surface is limited to Windows systems where GitPython's shell or hook features are used. An attacker who can place a malicious executable in a repository that is operated on by GitPython (e.g., via a crafted repository clone or open) could subvert the library's subprocess execution. The vulnerability arises because the environment is not properly sanitized to exclude the current working directory from the search path when shell=True or when running bash.exe [1][4].

Successful exploitation could lead to arbitrary code execution in the context of the user running GitPython. The attacker could gain the same privileges as the user performing Git operations, potentially compromising sensitive data or the system. This is a critical issue as it bypasses the prior fix for CVE-2023-40590 [3].

The issue is patched in GitPython version 3.1.41. The fix applies the NoDefaultCurrentDirectoryInExePath environment variable to subprocess calls to prevent CWD search, and addresses both shell and hook scenarios [1][4]. Users on Windows should upgrade immediately. No workarounds are provided [3].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
GitPythonPyPI
< 3.1.413.1.41

Affected products

4

Patches

1
ef3192cc414f

Merge pull request #1792 from EliahKagan/popen

https://github.com/gitpython-developers/GitPythonSebastian ThielJan 10, 2024via ghsa
9 files changed · +289 84
  • git/cmd.py+76 26 modified
    @@ -46,6 +46,7 @@
         Iterator,
         List,
         Mapping,
    +    Optional,
         Sequence,
         TYPE_CHECKING,
         TextIO,
    @@ -102,7 +103,7 @@ def handle_process_output(
             Callable[[bytes, "Repo", "DiffIndex"], None],
         ],
         stderr_handler: Union[None, Callable[[AnyStr], None], Callable[[List[AnyStr]], None]],
    -    finalizer: Union[None, Callable[[Union[subprocess.Popen, "Git.AutoInterrupt"]], None]] = None,
    +    finalizer: Union[None, Callable[[Union[Popen, "Git.AutoInterrupt"]], None]] = None,
         decode_streams: bool = True,
         kill_after_timeout: Union[None, float] = None,
     ) -> None:
    @@ -207,6 +208,68 @@ def pump_stream(
             finalizer(process)
     
     
    +def _safer_popen_windows(
    +    command: Union[str, Sequence[Any]],
    +    *,
    +    shell: bool = False,
    +    env: Optional[Mapping[str, str]] = None,
    +    **kwargs: Any,
    +) -> Popen:
    +    """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the search.
    +
    +    This avoids an untrusted search path condition where a file like ``git.exe`` in a
    +    malicious repository would be run when GitPython operates on the repository. The
    +    process using GitPython may have an untrusted repository's working tree as its
    +    current working directory. Some operations may temporarily change to that directory
    +    before running a subprocess. In addition, while by default GitPython does not run
    +    external commands with a shell, it can be made to do so, in which case the CWD of
    +    the subprocess, which GitPython usually sets to a repository working tree, can
    +    itself be searched automatically by the shell. This wrapper covers all those cases.
    +
    +    :note: This currently works by setting the ``NoDefaultCurrentDirectoryInExePath``
    +        environment variable during subprocess creation. It also takes care of passing
    +        Windows-specific process creation flags, but that is unrelated to path search.
    +
    +    :note: The current implementation contains a race condition on :attr:`os.environ`.
    +        GitPython isn't thread-safe, but a program using it on one thread should ideally
    +        be able to mutate :attr:`os.environ` on another, without unpredictable results.
    +        See comments in https://github.com/gitpython-developers/GitPython/pull/1650.
    +    """
    +    # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. See:
    +    # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
    +    # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
    +    creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
    +
    +    # When using a shell, the shell is the direct subprocess, so the variable must be
    +    # set in its environment, to affect its search behavior. (The "1" can be any value.)
    +    if shell:
    +        safer_env = {} if env is None else dict(env)
    +        safer_env["NoDefaultCurrentDirectoryInExePath"] = "1"
    +    else:
    +        safer_env = env
    +
    +    # When not using a shell, the current process does the search in a CreateProcessW
    +    # API call, so the variable must be set in our environment. With a shell, this is
    +    # unnecessary, in versions where https://github.com/python/cpython/issues/101283 is
    +    # patched. If not, in the rare case the ComSpec environment variable is unset, the
    +    # shell is searched for unsafely. Setting NoDefaultCurrentDirectoryInExePath in all
    +    # cases, as here, is simpler and protects against that. (The "1" can be any value.)
    +    with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
    +        return Popen(
    +            command,
    +            shell=shell,
    +            env=safer_env,
    +            creationflags=creationflags,
    +            **kwargs,
    +        )
    +
    +
    +if os.name == "nt":
    +    safer_popen = _safer_popen_windows
    +else:
    +    safer_popen = Popen
    +
    +
     def dashify(string: str) -> str:
         return string.replace("_", "-")
     
    @@ -225,14 +288,6 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
     ## -- End Utilities -- @}
     
     
    -if os.name == "nt":
    -    # CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards. See:
    -    # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
    -    PROC_CREATIONFLAGS = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
    -else:
    -    PROC_CREATIONFLAGS = 0
    -
    -
     class Git(LazyMixin):
         """The Git class manages communication with the Git binary.
     
    @@ -992,11 +1047,8 @@ def execute(
                         redacted_command,
                         '"kill_after_timeout" feature is not supported on Windows.',
                     )
    -            # Only search PATH, not CWD. This must be in the *caller* environment. The "1" can be any value.
    -            maybe_patch_caller_env = patch_env("NoDefaultCurrentDirectoryInExePath", "1")
             else:
                 cmd_not_found_exception = FileNotFoundError
    -            maybe_patch_caller_env = contextlib.nullcontext()
             # END handle
     
             stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
    @@ -1011,20 +1063,18 @@ def execute(
                 universal_newlines,
             )
             try:
    -            with maybe_patch_caller_env:
    -                proc = Popen(
    -                    command,
    -                    env=env,
    -                    cwd=cwd,
    -                    bufsize=-1,
    -                    stdin=(istream or DEVNULL),
    -                    stderr=PIPE,
    -                    stdout=stdout_sink,
    -                    shell=shell,
    -                    universal_newlines=universal_newlines,
    -                    creationflags=PROC_CREATIONFLAGS,
    -                    **subprocess_kwargs,
    -                )
    +            proc = safer_popen(
    +                command,
    +                env=env,
    +                cwd=cwd,
    +                bufsize=-1,
    +                stdin=(istream or DEVNULL),
    +                stderr=PIPE,
    +                stdout=stdout_sink,
    +                shell=shell,
    +                universal_newlines=universal_newlines,
    +                **subprocess_kwargs,
    +            )
             except cmd_not_found_exception as err:
                 raise GitCommandNotFound(redacted_command, err) from err
             else:
    
  • git/index/fun.py+2 3 modified
    @@ -18,7 +18,7 @@
     )
     import subprocess
     
    -from git.cmd import PROC_CREATIONFLAGS, handle_process_output
    +from git.cmd import handle_process_output, safer_popen
     from git.compat import defenc, force_bytes, force_text, safe_decode
     from git.exc import HookExecutionError, UnmergedEntriesError
     from git.objects.fun import (
    @@ -98,13 +98,12 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
                 relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix()
                 cmd = ["bash.exe", relative_hp]
     
    -        process = subprocess.Popen(
    +        process = safer_popen(
                 cmd + list(args),
                 env=env,
                 stdout=subprocess.PIPE,
                 stderr=subprocess.PIPE,
                 cwd=index.repo.working_dir,
    -            creationflags=PROC_CREATIONFLAGS,
             )
         except Exception as ex:
             raise HookExecutionError(hp, ex) from ex
    
  • git/util.py+11 0 modified
    @@ -327,6 +327,17 @@ def _get_exe_extensions() -> Sequence[str]:
     
     
     def py_where(program: str, path: Optional[PathLike] = None) -> List[str]:
    +    """Perform a path search to assist :func:`is_cygwin_git`.
    +
    +    This is not robust for general use. It is an implementation detail of
    +    :func:`is_cygwin_git`. When a search following all shell rules is needed,
    +    :func:`shutil.which` can be used instead.
    +
    +    :note: Neither this function nor :func:`shutil.which` will predict the effect of an
    +        executable search on a native Windows system due to a :class:`subprocess.Popen`
    +        call without ``shell=True``, because shell and non-shell executable search on
    +        Windows differ considerably.
    +    """
         # From: http://stackoverflow.com/a/377028/548792
         winprog_exts = _get_exe_extensions()
     
    
  • .pre-commit-config.yaml+1 1 modified
    @@ -29,7 +29,7 @@ repos:
       hooks:
       - id: shellcheck
         args: [--color]
    -    exclude: ^git/ext/
    +    exclude: ^test/fixtures/polyglot$|^git/ext/
     
     - repo: https://github.com/pre-commit/pre-commit-hooks
       rev: v4.4.0
    
  • test/fixtures/polyglot+8 0 added
    @@ -0,0 +1,8 @@
    +#!/usr/bin/env sh
    +# Valid script in both Bash and Python, but with different behavior.
    +""":"
    +echo 'Ran intended hook.' >output.txt
    +exit
    +" """
    +from pathlib import Path
    +Path('payload.txt').write_text('Ran impostor hook!', encoding='utf-8')
    
  • test/lib/helper.py+47 2 modified
    @@ -14,6 +14,7 @@
     import textwrap
     import time
     import unittest
    +import venv
     
     import gitdb
     
    @@ -36,6 +37,7 @@
         "with_rw_repo",
         "with_rw_and_rw_remote_repo",
         "TestBase",
    +    "VirtualEnvironment",
         "TestCase",
         "SkipTest",
         "skipIf",
    @@ -88,11 +90,11 @@ def with_rw_directory(func):
         test succeeds, but leave it otherwise to aid additional debugging."""
     
         @wraps(func)
    -    def wrapper(self):
    +    def wrapper(self, *args, **kwargs):
             path = tempfile.mkdtemp(prefix=func.__name__)
             keep = False
             try:
    -            return func(self, path)
    +            return func(self, path, *args, **kwargs)
             except Exception:
                 log.info(
                     "Test %s.%s failed, output is at %r\n",
    @@ -390,3 +392,46 @@ def _make_file(self, rela_path, data, repo=None):
             with open(abs_path, "w") as fp:
                 fp.write(data)
             return abs_path
    +
    +
    +class VirtualEnvironment:
    +    """A newly created Python virtual environment for use in a test."""
    +
    +    __slots__ = ("_env_dir",)
    +
    +    def __init__(self, env_dir, *, with_pip):
    +        if os.name == "nt":
    +            self._env_dir = osp.realpath(env_dir)
    +            venv.create(self.env_dir, symlinks=False, with_pip=with_pip)
    +        else:
    +            self._env_dir = env_dir
    +            venv.create(self.env_dir, symlinks=True, with_pip=with_pip)
    +
    +    @property
    +    def env_dir(self):
    +        """The top-level directory of the environment."""
    +        return self._env_dir
    +
    +    @property
    +    def python(self):
    +        """Path to the Python executable in the environment."""
    +        return self._executable("python")
    +
    +    @property
    +    def pip(self):
    +        """Path to the pip executable in the environment, or RuntimeError if absent."""
    +        return self._executable("pip")
    +
    +    @property
    +    def sources(self):
    +        """Path to a src directory in the environment, which may not exist yet."""
    +        return os.path.join(self.env_dir, "src")
    +
    +    def _executable(self, basename):
    +        if os.name == "nt":
    +            path = osp.join(self.env_dir, "Scripts", basename + ".exe")
    +        else:
    +            path = osp.join(self.env_dir, "bin", basename)
    +        if osp.isfile(path) or osp.islink(path):
    +            return path
    +        raise RuntimeError(f"no regular file or symlink {path!r}")
    
  • test/test_git.py+70 28 modified
    @@ -3,16 +3,18 @@
     # This module is part of GitPython and is released under the
     # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
     
    +import contextlib
     import gc
     import inspect
     import logging
     import os
     import os.path as osp
    +from pathlib import Path
     import re
     import shutil
     import subprocess
     import sys
    -from tempfile import TemporaryDirectory, TemporaryFile
    +from tempfile import TemporaryFile
     from unittest import skipUnless
     
     if sys.version_info >= (3, 8):
    @@ -27,6 +29,21 @@
     from test.lib import TestBase, fixture_path, with_rw_directory
     
     
    +@contextlib.contextmanager
    +def _patch_out_env(name):
    +    try:
    +        old_value = os.environ[name]
    +    except KeyError:
    +        old_value = None
    +    else:
    +        del os.environ[name]
    +    try:
    +        yield
    +    finally:
    +        if old_value is not None:
    +            os.environ[name] = old_value
    +
    +
     @ddt.ddt
     class TestGit(TestBase):
         @classmethod
    @@ -97,29 +114,28 @@ def test_it_transforms_kwargs_into_git_command_arguments(self):
     
         def _do_shell_combo(self, value_in_call, value_from_class):
             with mock.patch.object(Git, "USE_SHELL", value_from_class):
    -            # git.cmd gets Popen via a "from" import, so patch it there.
    -            with mock.patch.object(cmd, "Popen", wraps=cmd.Popen) as mock_popen:
    +            with mock.patch.object(cmd, "safer_popen", wraps=cmd.safer_popen) as mock_safer_popen:
                     # Use a command with no arguments (besides the program name), so it runs
                     # with or without a shell, on all OSes, with the same effect.
                     self.git.execute(["git"], with_exceptions=False, shell=value_in_call)
     
    -        return mock_popen
    +        return mock_safer_popen
     
         @ddt.idata(_shell_cases)
         def test_it_uses_shell_or_not_as_specified(self, case):
             """A bool passed as ``shell=`` takes precedence over `Git.USE_SHELL`."""
             value_in_call, value_from_class, expected_popen_arg = case
    -        mock_popen = self._do_shell_combo(value_in_call, value_from_class)
    -        mock_popen.assert_called_once()
    -        self.assertIs(mock_popen.call_args.kwargs["shell"], expected_popen_arg)
    +        mock_safer_popen = self._do_shell_combo(value_in_call, value_from_class)
    +        mock_safer_popen.assert_called_once()
    +        self.assertIs(mock_safer_popen.call_args.kwargs["shell"], expected_popen_arg)
     
         @ddt.idata(full_case[:2] for full_case in _shell_cases)
         def test_it_logs_if_it_uses_a_shell(self, case):
             """``shell=`` in the log message agrees with what is passed to `Popen`."""
             value_in_call, value_from_class = case
             with self.assertLogs(cmd.log, level=logging.DEBUG) as log_watcher:
    -            mock_popen = self._do_shell_combo(value_in_call, value_from_class)
    -        self._assert_logged_for_popen(log_watcher, "shell", mock_popen.call_args.kwargs["shell"])
    +            mock_safer_popen = self._do_shell_combo(value_in_call, value_from_class)
    +        self._assert_logged_for_popen(log_watcher, "shell", mock_safer_popen.call_args.kwargs["shell"])
     
         @ddt.data(
             ("None", None),
    @@ -134,22 +150,49 @@ def test_it_logs_istream_summary_for_stdin(self, case):
         def test_it_executes_git_and_returns_result(self):
             self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$")
     
    -    def test_it_executes_git_not_from_cwd(self):
    -        with TemporaryDirectory() as tmpdir:
    -            if os.name == "nt":
    -                # Copy an actual binary executable that is not git.
    -                other_exe_path = os.path.join(os.getenv("WINDIR"), "system32", "hostname.exe")
    -                impostor_path = os.path.join(tmpdir, "git.exe")
    -                shutil.copy(other_exe_path, impostor_path)
    -            else:
    -                # Create a shell script that doesn't do anything.
    -                impostor_path = os.path.join(tmpdir, "git")
    -                with open(impostor_path, mode="w", encoding="utf-8") as file:
    -                    print("#!/bin/sh", file=file)
    -                os.chmod(impostor_path, 0o755)
    -
    -            with cwd(tmpdir):
    -                self.assertRegex(self.git.execute(["git", "version"]), r"^git version\b")
    +    @ddt.data(
    +        # chdir_to_repo, shell, command, use_shell_impostor
    +        (False, False, ["git", "version"], False),
    +        (False, True, "git version", False),
    +        (False, True, "git version", True),
    +        (True, False, ["git", "version"], False),
    +        (True, True, "git version", False),
    +        (True, True, "git version", True),
    +    )
    +    @with_rw_directory
    +    def test_it_executes_git_not_from_cwd(self, rw_dir, case):
    +        chdir_to_repo, shell, command, use_shell_impostor = case
    +
    +        repo = Repo.init(rw_dir)
    +
    +        if os.name == "nt":
    +            # Copy an actual binary executable that is not git. (On Windows, running
    +            # "hostname" only displays the hostname, it never tries to change it.)
    +            other_exe_path = Path(os.environ["SystemRoot"], "system32", "hostname.exe")
    +            impostor_path = Path(rw_dir, "git.exe")
    +            shutil.copy(other_exe_path, impostor_path)
    +        else:
    +            # Create a shell script that doesn't do anything.
    +            impostor_path = Path(rw_dir, "git")
    +            impostor_path.write_text("#!/bin/sh\n", encoding="utf-8")
    +            os.chmod(impostor_path, 0o755)
    +
    +        if use_shell_impostor:
    +            shell_name = "cmd.exe" if os.name == "nt" else "sh"
    +            shutil.copy(impostor_path, Path(rw_dir, shell_name))
    +
    +        with contextlib.ExitStack() as stack:
    +            if chdir_to_repo:
    +                stack.enter_context(cwd(rw_dir))
    +            if use_shell_impostor:
    +                stack.enter_context(_patch_out_env("ComSpec"))
    +
    +            # Run the command without raising an exception on failure, as the exception
    +            # message is currently misleading when the command is a string rather than a
    +            # sequence of strings (it really runs "git", but then wrongly reports "g").
    +            output = repo.git.execute(command, with_exceptions=False, shell=shell)
    +
    +        self.assertRegex(output, r"^git version\b")
     
         @skipUnless(
             os.name == "nt",
    @@ -345,7 +388,7 @@ def test_environment(self, rw_dir):
                     self.assertIn("FOO", str(err))
     
         def test_handle_process_output(self):
    -        from git.cmd import handle_process_output
    +        from git.cmd import handle_process_output, safer_popen
     
             line_count = 5002
             count = [None, 0, 0]
    @@ -361,13 +404,12 @@ def counter_stderr(line):
                 fixture_path("cat_file.py"),
                 str(fixture_path("issue-301_stderr")),
             ]
    -        proc = subprocess.Popen(
    +        proc = safer_popen(
                 cmdline,
                 stdin=None,
                 stdout=subprocess.PIPE,
                 stderr=subprocess.PIPE,
                 shell=False,
    -            creationflags=cmd.PROC_CREATIONFLAGS,
             )
     
             handle_process_output(proc, counter_stdout, counter_stderr, finalize_process)
    
  • test/test_index.py+54 2 modified
    @@ -3,16 +3,19 @@
     # This module is part of GitPython and is released under the
     # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
     
    +import contextlib
     from io import BytesIO
     import logging
     import os
     import os.path as osp
     from pathlib import Path
     import re
    +import shutil
     from stat import S_ISLNK, ST_MODE
     import subprocess
     import tempfile
     
    +import ddt
     import pytest
     from sumtypes import constructor, sumtype
     
    @@ -36,9 +39,16 @@
     from git.index.typ import BaseIndexEntry, IndexEntry
     from git.index.util import TemporaryFileSwap
     from git.objects import Blob
    -from git.util import Actor, hex_to_bin, rmtree
    +from git.util import Actor, cwd, hex_to_bin, rmtree
     from gitdb.base import IStream
    -from test.lib import TestBase, fixture, fixture_path, with_rw_directory, with_rw_repo
    +from test.lib import (
    +    TestBase,
    +    VirtualEnvironment,
    +    fixture,
    +    fixture_path,
    +    with_rw_directory,
    +    with_rw_repo,
    +)
     
     HOOKS_SHEBANG = "#!/usr/bin/env sh\n"
     
    @@ -172,6 +182,7 @@ def _make_hook(git_dir, name, content, make_exec=True):
         return hp
     
     
    +@ddt.ddt
     class TestIndex(TestBase):
         def __init__(self, *args):
             super().__init__(*args)
    @@ -1012,6 +1023,47 @@ def test_run_commit_hook(self, rw_repo):
             output = Path(rw_repo.git_dir, "output.txt").read_text(encoding="utf-8")
             self.assertEqual(output, "ran fake hook\n")
     
    +    @ddt.data((False,), (True,))
    +    @with_rw_directory
    +    def test_hook_uses_shell_not_from_cwd(self, rw_dir, case):
    +        (chdir_to_repo,) = case
    +
    +        shell_name = "bash.exe" if os.name == "nt" else "sh"
    +        maybe_chdir = cwd(rw_dir) if chdir_to_repo else contextlib.nullcontext()
    +        repo = Repo.init(rw_dir)
    +
    +        # We need an impostor shell that works on Windows and that the test can
    +        # distinguish from the real bash.exe. But even if the real bash.exe is absent or
    +        # unusable, we should verify the impostor is not run. So the impostor needs a
    +        # clear side effect (unlike in TestGit.test_it_executes_git_not_from_cwd). Popen
    +        # on Windows uses CreateProcessW, which disregards PATHEXT; the impostor may
    +        # need to be a binary executable to ensure the vulnerability is found if
    +        # present. No compiler need exist, shipping a binary in the test suite may
    +        # target the wrong architecture, and generating one in a bespoke way may trigger
    +        # false positive virus scans. So we use a Bash/Python polyglot for the hook and
    +        # use the Python interpreter itself as the bash.exe impostor. But an interpreter
    +        # from a venv may not run when copied outside of it, and a global interpreter
    +        # won't run when copied to a different location if it was installed from the
    +        # Microsoft Store. So we make a new venv in rw_dir and use its interpreter.
    +        venv = VirtualEnvironment(rw_dir, with_pip=False)
    +        shutil.copy(venv.python, Path(rw_dir, shell_name))
    +        shutil.copy(fixture_path("polyglot"), hook_path("polyglot", repo.git_dir))
    +        payload = Path(rw_dir, "payload.txt")
    +
    +        if type(_win_bash_status) in {WinBashStatus.Absent, WinBashStatus.WslNoDistro}:
    +            # The real shell can't run, but the impostor should still not be used.
    +            with self.assertRaises(HookExecutionError):
    +                with maybe_chdir:
    +                    run_commit_hook("polyglot", repo.index)
    +            self.assertFalse(payload.exists())
    +        else:
    +            # The real shell should run, and not the impostor.
    +            with maybe_chdir:
    +                run_commit_hook("polyglot", repo.index)
    +            self.assertFalse(payload.exists())
    +            output = Path(rw_dir, "output.txt").read_text(encoding="utf-8")
    +            self.assertEqual(output, "Ran intended hook.\n")
    +
         @pytest.mark.xfail(
             type(_win_bash_status) is WinBashStatus.Absent,
             reason="Can't run a hook on Windows without bash.exe.",
    
  • test/test_installation.py+20 22 modified
    @@ -4,31 +4,19 @@
     import ast
     import os
     import subprocess
    -import sys
     
    -from test.lib import TestBase
    -from test.lib.helper import with_rw_directory
    +from test.lib import TestBase, VirtualEnvironment, with_rw_directory
     
     
     class TestInstallation(TestBase):
    -    def setUp_venv(self, rw_dir):
    -        self.venv = rw_dir
    -        subprocess.run([sys.executable, "-m", "venv", self.venv], stdout=subprocess.PIPE)
    -        bin_name = "Scripts" if os.name == "nt" else "bin"
    -        self.python = os.path.join(self.venv, bin_name, "python")
    -        self.pip = os.path.join(self.venv, bin_name, "pip")
    -        self.sources = os.path.join(self.venv, "src")
    -        self.cwd = os.path.dirname(os.path.dirname(__file__))
    -        os.symlink(self.cwd, self.sources, target_is_directory=True)
    -
         @with_rw_directory
         def test_installation(self, rw_dir):
    -        self.setUp_venv(rw_dir)
    +        venv = self._set_up_venv(rw_dir)
     
             result = subprocess.run(
    -            [self.pip, "install", "."],
    +            [venv.pip, "install", "."],
                 stdout=subprocess.PIPE,
    -            cwd=self.sources,
    +            cwd=venv.sources,
             )
             self.assertEqual(
                 0,
    @@ -37,9 +25,9 @@ def test_installation(self, rw_dir):
             )
     
             result = subprocess.run(
    -            [self.python, "-c", "import git"],
    +            [venv.python, "-c", "import git"],
                 stdout=subprocess.PIPE,
    -            cwd=self.sources,
    +            cwd=venv.sources,
             )
             self.assertEqual(
                 0,
    @@ -48,9 +36,9 @@ def test_installation(self, rw_dir):
             )
     
             result = subprocess.run(
    -            [self.python, "-c", "import gitdb; import smmap"],
    +            [venv.python, "-c", "import gitdb; import smmap"],
                 stdout=subprocess.PIPE,
    -            cwd=self.sources,
    +            cwd=venv.sources,
             )
             self.assertEqual(
                 0,
    @@ -62,9 +50,9 @@ def test_installation(self, rw_dir):
             # by inserting its location into PYTHONPATH or otherwise patched into
             # sys.path, make sure it is not wrongly inserted as the *first* entry.
             result = subprocess.run(
    -            [self.python, "-c", "import sys; import git; print(sys.path)"],
    +            [venv.python, "-c", "import sys; import git; print(sys.path)"],
                 stdout=subprocess.PIPE,
    -            cwd=self.sources,
    +            cwd=venv.sources,
             )
             syspath = result.stdout.decode("utf-8").splitlines()[0]
             syspath = ast.literal_eval(syspath)
    @@ -73,3 +61,13 @@ def test_installation(self, rw_dir):
                 syspath[0],
                 msg="Failed to follow the conventions for https://docs.python.org/3/library/sys.html#sys.path",
             )
    +
    +    @staticmethod
    +    def _set_up_venv(rw_dir):
    +        venv = VirtualEnvironment(rw_dir, with_pip=True)
    +        os.symlink(
    +            os.path.dirname(os.path.dirname(__file__)),
    +            venv.sources,
    +            target_is_directory=True,
    +        )
    +        return venv
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.