Skip to content

Commit

Permalink
Correct the "not from cwd" test and add more cases
Browse files Browse the repository at this point in the history
This shows that CVE-2023-40590 is only partially patched. This
commit does not fix the vulnerbility, only the test.

The problem is that, when shell=True, the environment of the shell
subprocess, instead of the GitPython process, determines whether
searching for the "git" command will use the current directory.
Currently NoDefaultCurrentDirectoryInExePath is set only in the
current process, and this is done after the environment for the
subprocess (env) is computed. So when git is an indirect subprocess
due to shell=True, Windows still checks a CWD when looking it up.

(Note that this should not be a problem for indirect subprocesses
started by git itself. When a standard git command is implemented
as an external executable, when git runs a custom command, and when
one git command delegates some of its work to another -- e.g.
"git clone" running git-remote-https -- Git for Windows already
does not search the current directory. Custom git commands that
start their own git subprocesses could have an analogous path
search bug, but this would be separate from the bug in GitPython.)

This is an exploitable vulnerability in GitPython. Although
shell=True is rarer and inherently less secure than the default of
shell=False, it still ought to avoid automatically running an
executable that may exist only due to having been cloned as part of
an untrusted repository. In addition, historically programs on
Windows had sometimes used shell=True as a workaround for console
windows being displayed for subprocesses, and some such code may
still be in use.

Furthermore, even when GitPython's current working directory is
outside the repository being worked on, the Git object in a Repo
instance's "git" attribute holds the repository directory as its
"_working_dir" attribute, which Git.execute consults to determine
the value of "cwd" to pass to subprocess.Popen. When the created
direct subprocess is really a shell, this is the CWD where git.exe
is looked for before searching PATH directories.

This is also why previous, more limited testing (including
accompanying manual testing with shell=True) didn't discover the
bug. Even when modified to test with shell=True, the old test had
the "impostor" git.exe in the GitPython process's own current
directory (which was changed to a temporary directory for the test,
where the "impostor" was created, but this was separate from the
working tree of the self.git repository). This was effective for
the shell=False case, but not the shell=True case where the
impostor would be found and run in the repository directory even
when it differs from the GitPython process's CWD.
  • Loading branch information
EliahKagan committed Dec 26, 2023
1 parent 1c65efb commit 2b47933
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 23 deletions.
4 changes: 2 additions & 2 deletions test/lib/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,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",
Expand Down
51 changes: 30 additions & 21 deletions test/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# 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
Expand All @@ -12,7 +13,7 @@
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):
Expand Down Expand Up @@ -135,27 +136,35 @@ def test_it_executes_git_and_returns_result(self):
self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$")

@ddt.data(
(["git", "version"], False),
("git version", True),
(False, False, ["git", "version"]),
(False, True, "git version"),
(True, False, ["git", "version"]),
(True, True, "git version"),
)
def test_it_executes_git_not_from_cwd(self, case):
command, shell = case

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):
output = self.git.execute(command, shell=shell)
@with_rw_directory
def test_it_executes_git_not_from_cwd(self, rw_dir, case):
chdir_to_repo, shell, command = 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 = os.path.join(os.getenv("WINDIR"), "system32", "hostname.exe")
impostor_path = os.path.join(rw_dir, "git.exe")
shutil.copy(other_exe_path, impostor_path)
else:
# Create a shell script that doesn't do anything.
impostor_path = os.path.join(rw_dir, "git")
with open(impostor_path, mode="w", encoding="utf-8") as file:
print("#!/bin/sh", file=file)
os.chmod(impostor_path, 0o755)

with cwd(rw_dir) if chdir_to_repo else contextlib.nullcontext():
# 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")

Expand Down

0 comments on commit 2b47933

Please sign in to comment.