47
47
Iterator ,
48
48
List ,
49
49
Mapping ,
50
+ Optional ,
50
51
Sequence ,
51
52
TYPE_CHECKING ,
52
53
TextIO ,
@@ -103,7 +104,7 @@ def handle_process_output(
103
104
Callable [[bytes , "Repo" , "DiffIndex" ], None ],
104
105
],
105
106
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 ,
107
108
decode_streams : bool = True ,
108
109
kill_after_timeout : Union [None , float ] = None ,
109
110
) -> None :
@@ -208,6 +209,68 @@ def pump_stream(
208
209
finalizer (process )
209
210
210
211
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
+
211
274
def dashify (string : str ) -> str :
212
275
return string .replace ("_" , "-" )
213
276
@@ -226,14 +289,6 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
226
289
## -- End Utilities -- @}
227
290
228
291
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
-
237
292
class Git (LazyMixin ):
238
293
"""The Git class manages communication with the Git binary.
239
294
@@ -1160,11 +1215,8 @@ def execute(
1160
1215
redacted_command ,
1161
1216
'"kill_after_timeout" feature is not supported on Windows.' ,
1162
1217
)
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" )
1165
1218
else :
1166
1219
cmd_not_found_exception = FileNotFoundError
1167
- maybe_patch_caller_env = contextlib .nullcontext ()
1168
1220
# END handle
1169
1221
1170
1222
stdout_sink = PIPE if with_stdout else getattr (subprocess , "DEVNULL" , None ) or open (os .devnull , "wb" )
@@ -1179,20 +1231,18 @@ def execute(
1179
1231
universal_newlines ,
1180
1232
)
1181
1233
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
+ )
1196
1246
except cmd_not_found_exception as err :
1197
1247
raise GitCommandNotFound (redacted_command , err ) from err
1198
1248
else :
0 commit comments