|  | 
| 29 | 29 |     cygpath, | 
| 30 | 30 |     expand_path, | 
| 31 | 31 |     is_cygwin_git, | 
|  | 32 | +    patch_env, | 
| 32 | 33 |     remove_password_if_present, | 
| 33 |  | -    safer_popen, | 
| 34 | 34 |     stream_copy, | 
| 35 | 35 | ) | 
| 36 | 36 | 
 | 
|  | 
| 46 | 46 |     Iterator, | 
| 47 | 47 |     List, | 
| 48 | 48 |     Mapping, | 
|  | 49 | +    Optional, | 
| 49 | 50 |     Sequence, | 
| 50 | 51 |     TYPE_CHECKING, | 
| 51 | 52 |     TextIO, | 
| @@ -102,7 +103,7 @@ def handle_process_output( | 
| 102 | 103 |         Callable[[bytes, "Repo", "DiffIndex"], None], | 
| 103 | 104 |     ], | 
| 104 | 105 |     stderr_handler: Union[None, Callable[[AnyStr], None], Callable[[List[AnyStr]], None]], | 
| 105 |  | -    finalizer: Union[None, Callable[[Union[subprocess.Popen, "Git.AutoInterrupt"]], None]] = None, | 
|  | 106 | +    finalizer: Union[None, Callable[[Union[Popen, "Git.AutoInterrupt"]], None]] = None, | 
| 106 | 107 |     decode_streams: bool = True, | 
| 107 | 108 |     kill_after_timeout: Union[None, float] = None, | 
| 108 | 109 | ) -> None: | 
| @@ -207,6 +208,68 @@ def pump_stream( | 
| 207 | 208 |         finalizer(process) | 
| 208 | 209 | 
 | 
| 209 | 210 | 
 | 
|  | 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 | + | 
| 210 | 273 | def dashify(string: str) -> str: | 
| 211 | 274 |     return string.replace("_", "-") | 
| 212 | 275 | 
 | 
|  | 
0 commit comments