4747 Iterator ,
4848 List ,
4949 Mapping ,
50+ Optional ,
5051 Sequence ,
5152 TYPE_CHECKING ,
5253 TextIO ,
@@ -103,7 +104,7 @@ def handle_process_output(
103104 Callable [[bytes , "Repo" , "DiffIndex" ], None ],
104105 ],
105106 stderr_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 ,
107108 decode_streams : bool = True ,
108109 kill_after_timeout : Union [None , float ] = None ,
109110) -> None :
@@ -208,6 +209,68 @@ def pump_stream(
208209 finalizer (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 :
212275 return 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(
11601215 redacted_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" )
11651218 else :
11661219 cmd_not_found_exception = FileNotFoundError
1167- maybe_patch_caller_env = contextlib .nullcontext ()
11681220 # END handle
11691221
11701222 stdout_sink = PIPE if with_stdout else getattr (subprocess , "DEVNULL" , None ) or open (os .devnull , "wb" )
@@ -1179,20 +1231,18 @@ def execute(
11791231 universal_newlines ,
11801232 )
11811233 try :
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+ )
11961246 except cmd_not_found_exception as err :
11971247 raise GitCommandNotFound (redacted_command , err ) from err
11981248 else :
0 commit comments