Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit a06be30

Browse filesBrowse files
committed
patched 3.1.37
1 parent 3faea5a commit a06be30
Copy full SHA for a06be30

13 files changed

+259-91Lines changed: 259 additions & 91 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎.github/workflows/cygwin-test.yml‎

Copy file name to clipboardExpand all lines: .github/workflows/cygwin-test.yml
+2-1Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on: [push, pull_request, workflow_dispatch]
44

55
jobs:
66
build:
7-
runs-on: windows-latest
7+
runs-on: windows-2022
88
strategy:
99
fail-fast: false
1010
env:
@@ -61,4 +61,5 @@ jobs:
6161
- name: Test with pytest
6262
run: |
6363
set +x
64+
git config --global --add safe.directory '*'
6465
/usr/bin/python -m pytest
Collapse file

‎.github/workflows/lint.yml‎

Copy file name to clipboardExpand all lines: .github/workflows/lint.yml
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ on: [push, pull_request, workflow_dispatch]
44

55
jobs:
66
lint:
7-
runs-on: ubuntu-latest
7+
runs-on: ubuntu-22.04
88

99
steps:
1010
- uses: actions/checkout@v4
11-
- uses: actions/setup-python@v4
11+
- uses: MatteoH2O1999/setup-python@v4
1212
with:
1313
python-version: "3.x"
1414
- uses: pre-commit/action@v3.0.0
Collapse file

‎.github/workflows/pythonpackage.yml‎

Copy file name to clipboardExpand all lines: .github/workflows/pythonpackage.yml
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ permissions:
1111
jobs:
1212
build:
1313

14-
runs-on: ubuntu-latest
14+
runs-on: ubuntu-22.04
1515
strategy:
1616
fail-fast: false
1717
matrix:
@@ -31,7 +31,7 @@ jobs:
3131
submodules: recursive
3232

3333
- name: Set up Python ${{ matrix.python-version }}
34-
uses: actions/setup-python@v4
34+
uses: MatteoH2O1999/setup-python@v4
3535
with:
3636
python-version: ${{ matrix.python-version }}
3737
allow-prereleases: ${{ matrix.experimental }}
Collapse file

‎.pre-commit-config.yaml‎

Copy file name to clipboardExpand all lines: .pre-commit-config.yaml
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ repos:
99
flake8-comprehensions==3.14.0,
1010
flake8-typing-imports==1.14.0,
1111
]
12-
exclude: ^doc|^git/ext/
12+
exclude: ^test/fixtures/polyglot$|^doc|^git/ext/
1313

1414
- repo: https://github.com/pre-commit/pre-commit-hooks
1515
rev: v4.4.0
Collapse file

‎VERSION‎

Copy file name to clipboard
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.1.37
1+
3.1.37+sp1
Collapse file

‎git/cmd.py‎

Copy file name to clipboardExpand all lines: git/cmd.py
+77-35Lines changed: 77 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
defenc,
2020
force_bytes,
2121
safe_decode,
22-
is_posix,
2322
is_win,
2423
)
2524
from git.exc import CommandError
@@ -43,6 +42,7 @@
4342
Iterator,
4443
List,
4544
Mapping,
45+
Optional,
4646
Sequence,
4747
TYPE_CHECKING,
4848
TextIO,
@@ -99,7 +99,7 @@ def handle_process_output(
9999
Callable[[bytes, "Repo", "DiffIndex"], None],
100100
],
101101
stderr_handler: Union[None, Callable[[AnyStr], None], Callable[[List[AnyStr]], None]],
102-
finalizer: Union[None, Callable[[Union[subprocess.Popen, "Git.AutoInterrupt"]], None]] = None,
102+
finalizer: Union[None, Callable[[Union[Popen, "Git.AutoInterrupt"]], None]] = None,
103103
decode_streams: bool = True,
104104
kill_after_timeout: Union[None, float] = None,
105105
) -> None:
@@ -207,6 +207,68 @@ def pump_stream(
207207
return None
208208

209209

210+
def _safer_popen_windows(
211+
command: Union[str, Sequence[Any]],
212+
*,
213+
shell: bool = False,
214+
env: Optional[Mapping[str, str]] = None,
215+
**kwargs: Any,
216+
) -> Popen:
217+
"""Call :class:`subprocess.Popen` on Windows but don't include a CWD in the search.
218+
219+
This avoids an untrusted search path condition where a file like ``git.exe`` in a
220+
malicious repository would be run when GitPython operates on the repository. The
221+
process using GitPython may have an untrusted repository's working tree as its
222+
current working directory. Some operations may temporarily change to that directory
223+
before running a subprocess. In addition, while by default GitPython does not run
224+
external commands with a shell, it can be made to do so, in which case the CWD of
225+
the subprocess, which GitPython usually sets to a repository working tree, can
226+
itself be searched automatically by the shell. This wrapper covers all those cases.
227+
228+
:note: This currently works by setting the ``NoDefaultCurrentDirectoryInExePath``
229+
environment variable during subprocess creation. It also takes care of passing
230+
Windows-specific process creation flags, but that is unrelated to path search.
231+
232+
:note: The current implementation contains a race condition on :attr:`os.environ`.
233+
GitPython isn't thread-safe, but a program using it on one thread should ideally
234+
be able to mutate :attr:`os.environ` on another, without unpredictable results.
235+
See comments in https://github.com/gitpython-developers/GitPython/pull/1650.
236+
"""
237+
# CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. See:
238+
# https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
239+
# https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
240+
creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
241+
242+
# When using a shell, the shell is the direct subprocess, so the variable must be
243+
# set in its environment, to affect its search behavior. (The "1" can be any value.)
244+
if shell:
245+
safer_env = {} if env is None else dict(env)
246+
safer_env["NoDefaultCurrentDirectoryInExePath"] = "1"
247+
else:
248+
safer_env = env
249+
250+
# When not using a shell, the current process does the search in a CreateProcessW
251+
# API call, so the variable must be set in our environment. With a shell, this is
252+
# unnecessary, in versions where https://github.com/python/cpython/issues/101283 is
253+
# patched. If not, in the rare case the ComSpec environment variable is unset, the
254+
# shell is searched for unsafely. Setting NoDefaultCurrentDirectoryInExePath in all
255+
# cases, as here, is simpler and protects against that. (The "1" can be any value.)
256+
with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
257+
return Popen(
258+
command,
259+
shell=shell,
260+
env=safer_env,
261+
creationflags=creationflags,
262+
**kwargs,
263+
)
264+
265+
266+
if os.name == "nt":
267+
safer_popen = _safer_popen_windows
268+
else:
269+
safer_popen = Popen
270+
271+
210272
def dashify(string: str) -> str:
211273
return string.replace("_", "-")
212274

@@ -225,16 +287,6 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
225287
## -- End Utilities -- @}
226288

227289

228-
# value of Windows process creation flag taken from MSDN
229-
CREATE_NO_WINDOW = 0x08000000
230-
231-
## CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards,
232-
# see https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
233-
PROC_CREATIONFLAGS = (
234-
CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP if is_win else 0 # type: ignore[attr-defined]
235-
) # mypy error if not windows
236-
237-
238290
class Git(LazyMixin):
239291

240292
"""
@@ -963,11 +1015,8 @@ def execute(
9631015
redacted_command,
9641016
'"kill_after_timeout" feature is not supported on Windows.',
9651017
)
966-
# Only search PATH, not CWD. This must be in the *caller* environment. The "1" can be any value.
967-
maybe_patch_caller_env = patch_env("NoDefaultCurrentDirectoryInExePath", "1")
9681018
else:
9691019
cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable
970-
maybe_patch_caller_env = contextlib.nullcontext()
9711020
# end handle
9721021

9731022
stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
@@ -983,21 +1032,18 @@ def execute(
9831032
istream_ok,
9841033
)
9851034
try:
986-
with maybe_patch_caller_env:
987-
proc = Popen(
988-
command,
989-
env=env,
990-
cwd=cwd,
991-
bufsize=-1,
992-
stdin=istream or DEVNULL,
993-
stderr=PIPE,
994-
stdout=stdout_sink,
995-
shell=shell is not None and shell or self.USE_SHELL,
996-
close_fds=is_posix, # unsupported on windows
997-
universal_newlines=universal_newlines,
998-
creationflags=PROC_CREATIONFLAGS,
999-
**subprocess_kwargs,
1000-
)
1035+
proc = safer_popen(
1036+
command,
1037+
env=env,
1038+
cwd=cwd,
1039+
bufsize=-1,
1040+
stdin=(istream or DEVNULL),
1041+
stderr=PIPE,
1042+
stdout=stdout_sink,
1043+
shell=shell,
1044+
universal_newlines=universal_newlines,
1045+
**subprocess_kwargs,
1046+
)
10011047
except cmd_not_found_exception as err:
10021048
raise GitCommandNotFound(redacted_command, err) from err
10031049
else:
@@ -1010,11 +1056,7 @@ def execute(
10101056

10111057
def _kill_process(pid: int) -> None:
10121058
"""Callback method to kill a process."""
1013-
p = Popen(
1014-
["ps", "--ppid", str(pid)],
1015-
stdout=PIPE,
1016-
creationflags=PROC_CREATIONFLAGS,
1017-
)
1059+
p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE)
10181060
child_pids = []
10191061
if p.stdout is not None:
10201062
for line in p.stdout:
Collapse file

‎git/index/fun.py‎

Copy file name to clipboardExpand all lines: git/index/fun.py
+3-3Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717
import subprocess
1818

19-
from git.cmd import PROC_CREATIONFLAGS, handle_process_output
19+
from git.cmd import handle_process_output, safer_popen
2020
from git.compat import (
2121
defenc,
2222
force_text,
@@ -102,14 +102,14 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
102102
relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix()
103103
cmd = ["bash.exe", relative_hp]
104104

105-
process = subprocess.Popen(
105+
# process = subprocess.Popen(
106+
process = safer_popen(
106107
cmd + list(args),
107108
env=env,
108109
stdout=subprocess.PIPE,
109110
stderr=subprocess.PIPE,
110111
cwd=index.repo.working_dir,
111112
close_fds=is_posix,
112-
creationflags=PROC_CREATIONFLAGS,
113113
)
114114
except Exception as ex:
115115
raise HookExecutionError(hp, ex) from ex
Collapse file

‎git/util.py‎

Copy file name to clipboardExpand all lines: git/util.py
+11Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,17 @@ def _get_exe_extensions() -> Sequence[str]:
291291

292292

293293
def py_where(program: str, path: Optional[PathLike] = None) -> List[str]:
294+
"""Perform a path search to assist :func:`is_cygwin_git`.
295+
296+
This is not robust for general use. It is an implementation detail of
297+
:func:`is_cygwin_git`. When a search following all shell rules is needed,
298+
:func:`shutil.which` can be used instead.
299+
300+
:note: Neither this function nor :func:`shutil.which` will predict the effect of an
301+
executable search on a native Windows system due to a :class:`subprocess.Popen`
302+
call without ``shell=True``, because shell and non-shell executable search on
303+
Windows differ considerably.
304+
"""
294305
# From: http://stackoverflow.com/a/377028/548792
295306
winprog_exts = _get_exe_extensions()
296307

Collapse file

‎test/fixtures/polyglot‎

Copy file name to clipboard
+8Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env sh
2+
# Valid script in both Bash and Python, but with different behavior.
3+
""":"
4+
echo 'Ran intended hook.' >output.txt
5+
exit
6+
" """
7+
from pathlib import Path
8+
Path('payload.txt').write_text('Ran impostor hook!', encoding='utf-8')
Collapse file

‎test/lib/helper.py‎

Copy file name to clipboardExpand all lines: test/lib/helper.py
+47-2Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import textwrap
1414
import time
1515
import unittest
16+
import venv
1617

1718
from git.compat import is_win
1819
from git.util import rmtree, cwd
@@ -38,6 +39,7 @@
3839
"with_rw_repo",
3940
"with_rw_and_rw_remote_repo",
4041
"TestBase",
42+
"VirtualEnvironment",
4143
"TestCase",
4244
"SkipTest",
4345
"skipIf",
@@ -89,12 +91,12 @@ def with_rw_directory(func):
8991
test succeeds, but leave it otherwise to aid additional debugging"""
9092

9193
@wraps(func)
92-
def wrapper(self):
94+
def wrapper(self, *args, **kwargs):
9395
path = tempfile.mktemp(prefix=func.__name__)
9496
os.mkdir(path)
9597
keep = False
9698
try:
97-
return func(self, path)
99+
return func(self, path, *args, **kwargs)
98100
except Exception:
99101
log.info(
100102
"Test %s.%s failed, output is at %r\n",
@@ -401,3 +403,46 @@ def _make_file(self, rela_path, data, repo=None):
401403
with open(abs_path, "w") as fp:
402404
fp.write(data)
403405
return abs_path
406+
407+
408+
class VirtualEnvironment:
409+
"""A newly created Python virtual environment for use in a test."""
410+
411+
__slots__ = ("_env_dir",)
412+
413+
def __init__(self, env_dir, *, with_pip):
414+
if os.name == "nt":
415+
self._env_dir = osp.realpath(env_dir)
416+
venv.create(self.env_dir, symlinks=False, with_pip=with_pip)
417+
else:
418+
self._env_dir = env_dir
419+
venv.create(self.env_dir, symlinks=True, with_pip=with_pip)
420+
421+
@property
422+
def env_dir(self):
423+
"""The top-level directory of the environment."""
424+
return self._env_dir
425+
426+
@property
427+
def python(self):
428+
"""Path to the Python executable in the environment."""
429+
return self._executable("python")
430+
431+
@property
432+
def pip(self):
433+
"""Path to the pip executable in the environment, or RuntimeError if absent."""
434+
return self._executable("pip")
435+
436+
@property
437+
def sources(self):
438+
"""Path to a src directory in the environment, which may not exist yet."""
439+
return os.path.join(self.env_dir, "src")
440+
441+
def _executable(self, basename):
442+
if os.name == "nt":
443+
path = osp.join(self.env_dir, "Scripts", basename + ".exe")
444+
else:
445+
path = osp.join(self.env_dir, "bin", basename)
446+
if osp.isfile(path) or osp.islink(path):
447+
return path
448+
raise RuntimeError(f"no regular file or symlink {path!r}")

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.