4747Iterator ,
4848List ,
4949Mapping ,
50+ Optional ,
5051Sequence ,
5152TYPE_CHECKING ,
5253TextIO ,
@@ -103,7 +104,7 @@ def handle_process_output(
103104Callable [[bytes ,"Repo" ,"DiffIndex" ],None ],
104105 ],
105106stderr_handler :Union [None ,Callable [[AnyStr ],None ],Callable [[List [AnyStr ]],None ]],
106- finalizer :Union [None ,Callable [[Union [subprocess . Popen ,"Git.AutoInterrupt" ]],None ]]= None ,
107+ finalizer :Union [None ,Callable [[Union [Popen ,"Git.AutoInterrupt" ]],None ]]= None ,
107108decode_streams :bool = True ,
108109kill_after_timeout :Union [None ,float ]= None ,
109110)-> None :
@@ -208,6 +209,68 @@ def pump_stream(
208209finalizer (process )
209210
210211
212+ def _safer_popen_windows (
213+ command :Union [str ,Sequence [Any ]],
214+ * ,
215+ shell :bool = False ,
216+ env :Optional [Mapping [str ,str ]]= None ,
217+ ** kwargs :Any ,
218+ )-> Popen :
219+ """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the search.
220+
221+ This avoids an untrusted search path condition where a file like ``git.exe`` in a
222+ malicious repository would be run when GitPython operates on the repository. The
223+ process using GitPython may have an untrusted repository's working tree as its
224+ current working directory. Some operations may temporarily change to that directory
225+ before running a subprocess. In addition, while by default GitPython does not run
226+ external commands with a shell, it can be made to do so, in which case the CWD of
227+ the subprocess, which GitPython usually sets to a repository working tree, can
228+ itself be searched automatically by the shell. This wrapper covers all those cases.
229+
230+ :note: This currently works by setting the ``NoDefaultCurrentDirectoryInExePath``
231+ environment variable during subprocess creation. It also takes care of passing
232+ Windows-specific process creation flags, but that is unrelated to path search.
233+
234+ :note: The current implementation contains a race condition on :attr:`os.environ`.
235+ GitPython isn't thread-safe, but a program using it on one thread should ideally
236+ be able to mutate :attr:`os.environ` on another, without unpredictable results.
237+ See comments in https://github.com/gitpython-developers/GitPython/pull/1650.
238+ """
239+ # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. See:
240+ # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
241+ # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
242+ creationflags = subprocess .CREATE_NO_WINDOW | subprocess .CREATE_NEW_PROCESS_GROUP
243+
244+ # When using a shell, the shell is the direct subprocess, so the variable must be
245+ # set in its environment, to affect its search behavior. (The "1" can be any value.)
246+ if shell :
247+ safer_env = {}if env is None else dict (env )
248+ safer_env ["NoDefaultCurrentDirectoryInExePath" ]= "1"
249+ else :
250+ safer_env = env
251+
252+ # When not using a shell, the current process does the search in a CreateProcessW
253+ # API call, so the variable must be set in our environment. With a shell, this is
254+ # unnecessary, in versions where https://github.com/python/cpython/issues/101283 is
255+ # patched. If not, in the rare case the ComSpec environment variable is unset, the
256+ # shell is searched for unsafely. Setting NoDefaultCurrentDirectoryInExePath in all
257+ # cases, as here, is simpler and protects against that. (The "1" can be any value.)
258+ with patch_env ("NoDefaultCurrentDirectoryInExePath" ,"1" ):
259+ return Popen (
260+ command ,
261+ shell = shell ,
262+ env = safer_env ,
263+ creationflags = creationflags ,
264+ ** kwargs ,
265+ )
266+
267+
268+ if os .name == "nt" :
269+ safer_popen = _safer_popen_windows
270+ else :
271+ safer_popen = Popen
272+
273+
211274def dashify (string :str )-> str :
212275return string .replace ("_" ,"-" )
213276
@@ -226,14 +289,6 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
226289## -- End Utilities -- @}
227290
228291
229- if os .name == "nt" :
230- # CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards. See:
231- # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
232- PROC_CREATIONFLAGS = subprocess .CREATE_NO_WINDOW | subprocess .CREATE_NEW_PROCESS_GROUP
233- else :
234- PROC_CREATIONFLAGS = 0
235-
236-
237292class Git (LazyMixin ):
238293"""The Git class manages communication with the Git binary.
239294
@@ -1160,11 +1215,8 @@ def execute(
11601215redacted_command ,
11611216'"kill_after_timeout" feature is not supported on Windows.' ,
11621217 )
1163- # Only search PATH, not CWD. This must be in the *caller* environment. The "1" can be any value.
1164- maybe_patch_caller_env = patch_env ("NoDefaultCurrentDirectoryInExePath" ,"1" )
11651218else :
11661219cmd_not_found_exception = FileNotFoundError
1167- maybe_patch_caller_env = contextlib .nullcontext ()
11681220# END handle
11691221
11701222stdout_sink = PIPE if with_stdout else getattr (subprocess ,"DEVNULL" ,None )or open (os .devnull ,"wb" )
@@ -1179,20 +1231,18 @@ def execute(
11791231universal_newlines ,
11801232 )
11811233try :
1182- with maybe_patch_caller_env :
1183- proc = Popen (
1184- command ,
1185- env = env ,
1186- cwd = cwd ,
1187- bufsize = - 1 ,
1188- stdin = (istream or DEVNULL ),
1189- stderr = PIPE ,
1190- stdout = stdout_sink ,
1191- shell = shell ,
1192- universal_newlines = universal_newlines ,
1193- creationflags = PROC_CREATIONFLAGS ,
1194- ** subprocess_kwargs ,
1195- )
1234+ proc = safer_popen (
1235+ command ,
1236+ env = env ,
1237+ cwd = cwd ,
1238+ bufsize = - 1 ,
1239+ stdin = (istream or DEVNULL ),
1240+ stderr = PIPE ,
1241+ stdout = stdout_sink ,
1242+ shell = shell ,
1243+ universal_newlines = universal_newlines ,
1244+ ** subprocess_kwargs ,
1245+ )
11961246except cmd_not_found_exception as err :
11971247raise GitCommandNotFound (redacted_command ,err )from err
11981248else :