4343 Iterator ,
4444 List ,
4545 Mapping ,
46+ Optional ,
4647 Sequence ,
4748 TYPE_CHECKING ,
4849 TextIO ,
@@ -99,7 +100,7 @@ def handle_process_output(
99100 Callable [[bytes , "Repo" , "DiffIndex" ], None ],
100101 ],
101102 stderr_handler : Union [None , Callable [[AnyStr ], None ], Callable [[List [AnyStr ]], None ]],
102- finalizer : Union [None , Callable [[Union [subprocess . Popen , "Git.AutoInterrupt" ]], None ]] = None ,
103+ finalizer : Union [None , Callable [[Union [Popen , "Git.AutoInterrupt" ]], None ]] = None ,
103104 decode_streams : bool = True ,
104105 kill_after_timeout : Union [None , float ] = None ,
105106) -> None :
@@ -207,6 +208,68 @@ def pump_stream(
207208 return None
208209
209210
211+ def _safer_popen_windows (
212+ command : Union [str , Sequence [Any ]],
213+ * ,
214+ shell : bool = False ,
215+ env : Optional [Mapping [str , str ]] = None ,
216+ ** kwargs : Any ,
217+ ) -> Popen :
218+ """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the search.
219+
220+ This avoids an untrusted search path condition where a file like ``git.exe`` in a
221+ malicious repository would be run when GitPython operates on the repository. The
222+ process using GitPython may have an untrusted repository's working tree as its
223+ current working directory. Some operations may temporarily change to that directory
224+ before running a subprocess. In addition, while by default GitPython does not run
225+ external commands with a shell, it can be made to do so, in which case the CWD of
226+ the subprocess, which GitPython usually sets to a repository working tree, can
227+ itself be searched automatically by the shell. This wrapper covers all those cases.
228+
229+ :note: This currently works by setting the ``NoDefaultCurrentDirectoryInExePath``
230+ environment variable during subprocess creation. It also takes care of passing
231+ Windows-specific process creation flags, but that is unrelated to path search.
232+
233+ :note: The current implementation contains a race condition on :attr:`os.environ`.
234+ GitPython isn't thread-safe, but a program using it on one thread should ideally
235+ be able to mutate :attr:`os.environ` on another, without unpredictable results.
236+ See comments in https://github.com/gitpython-developers/GitPython/pull/1650.
237+ """
238+ # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. See:
239+ # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
240+ # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
241+ creationflags = subprocess .CREATE_NO_WINDOW | subprocess .CREATE_NEW_PROCESS_GROUP
242+
243+ # When using a shell, the shell is the direct subprocess, so the variable must be
244+ # set in its environment, to affect its search behavior. (The "1" can be any value.)
245+ if shell :
246+ safer_env = {} if env is None else dict (env )
247+ safer_env ["NoDefaultCurrentDirectoryInExePath" ] = "1"
248+ else :
249+ safer_env = env
250+
251+ # When not using a shell, the current process does the search in a CreateProcessW
252+ # API call, so the variable must be set in our environment. With a shell, this is
253+ # unnecessary, in versions where https://github.com/python/cpython/issues/101283 is
254+ # patched. If not, in the rare case the ComSpec environment variable is unset, the
255+ # shell is searched for unsafely. Setting NoDefaultCurrentDirectoryInExePath in all
256+ # cases, as here, is simpler and protects against that. (The "1" can be any value.)
257+ with patch_env ("NoDefaultCurrentDirectoryInExePath" , "1" ):
258+ return Popen (
259+ command ,
260+ shell = shell ,
261+ env = safer_env ,
262+ creationflags = creationflags ,
263+ ** kwargs ,
264+ )
265+
266+
267+ if os .name == "nt" :
268+ safer_popen = _safer_popen_windows
269+ else :
270+ safer_popen = Popen
271+
272+
210273def dashify (string : str ) -> str :
211274 return string .replace ("_" , "-" )
212275
@@ -225,16 +288,6 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
225288## -- End Utilities -- @}
226289
227290
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-
238291class Git (LazyMixin ):
239292
240293 """
@@ -963,11 +1016,8 @@ def execute(
9631016 redacted_command ,
9641017 '"kill_after_timeout" feature is not supported on Windows.' ,
9651018 )
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" )
9681019 else :
9691020 cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable
970- maybe_patch_caller_env = contextlib .nullcontext ()
9711021 # end handle
9721022
9731023 stdout_sink = PIPE if with_stdout else getattr (subprocess , "DEVNULL" , None ) or open (os .devnull , "wb" )
@@ -983,21 +1033,18 @@ def execute(
9831033 istream_ok ,
9841034 )
9851035 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- )
1036+ proc = safer_popen (
1037+ command ,
1038+ env = env ,
1039+ cwd = cwd ,
1040+ bufsize = - 1 ,
1041+ stdin = (istream or DEVNULL ),
1042+ stderr = PIPE ,
1043+ stdout = stdout_sink ,
1044+ shell = shell ,
1045+ universal_newlines = universal_newlines ,
1046+ ** subprocess_kwargs ,
1047+ )
10011048 except cmd_not_found_exception as err :
10021049 raise GitCommandNotFound (redacted_command , err ) from err
10031050 else :
0 commit comments