|  | 
| 33 | 33 |     IO, | 
| 34 | 34 |     Iterator, | 
| 35 | 35 |     List, | 
|  | 36 | +    Mapping, | 
| 36 | 37 |     Optional, | 
| 37 | 38 |     Pattern, | 
| 38 | 39 |     Sequence, | 
| @@ -327,6 +328,17 @@ def _get_exe_extensions() -> Sequence[str]: | 
| 327 | 328 | 
 | 
| 328 | 329 | 
 | 
| 329 | 330 | def py_where(program: str, path: Optional[PathLike] = None) -> List[str]: | 
|  | 331 | +    """Perform a path search to assist :func:`is_cygwin_git`. | 
|  | 332 | +
 | 
|  | 333 | +    This is not robust for general use. It is an implementation detail of | 
|  | 334 | +    :func:`is_cygwin_git`. When a search following all shell rules is needed, | 
|  | 335 | +    :func:`shutil.which` can be used instead. | 
|  | 336 | +
 | 
|  | 337 | +    :note: Neither this function nor :func:`shutil.which` will predict the effect of an | 
|  | 338 | +        executable search on a native Windows system due to a :class:`subprocess.Popen` | 
|  | 339 | +        call without ``shell=True``, because shell and non-shell executable search on | 
|  | 340 | +        Windows differ considerably. | 
|  | 341 | +    """ | 
| 330 | 342 |     # From: http://stackoverflow.com/a/377028/548792 | 
| 331 | 343 |     winprog_exts = _get_exe_extensions() | 
| 332 | 344 | 
 | 
| @@ -524,6 +536,67 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]: | 
| 524 | 536 |     return new_cmdline | 
| 525 | 537 | 
 | 
| 526 | 538 | 
 | 
|  | 539 | +def _safer_popen_windows( | 
|  | 540 | +    command: Union[str, Sequence[Any]], | 
|  | 541 | +    *, | 
|  | 542 | +    shell: bool = False, | 
|  | 543 | +    env: Optional[Mapping[str, str]] = None, | 
|  | 544 | +    **kwargs: Any, | 
|  | 545 | +) -> subprocess.Popen: | 
|  | 546 | +    """Call :class:`subprocess.Popen` on Windows but don't include a CWD in the search. | 
|  | 547 | +
 | 
|  | 548 | +    This avoids an untrusted search path condition where a file like ``git.exe`` in a | 
|  | 549 | +    malicious repository would be run when GitPython operates on the repository. The | 
|  | 550 | +    process using GitPython may have an untrusted repository's working tree as its | 
|  | 551 | +    current working directory. Some operations may temporarily change to that directory | 
|  | 552 | +    before running a subprocess. In addition, while by default GitPython does not run | 
|  | 553 | +    external commands with a shell, it can be made to do so, in which case the CWD of | 
|  | 554 | +    the subprocess, which GitPython usually sets to a repository working tree, can | 
|  | 555 | +    itself be searched automatically by the shell. This wrapper covers all those cases. | 
|  | 556 | +
 | 
|  | 557 | +    :note: This currently works by setting the ``NoDefaultCurrentDirectoryInExePath`` | 
|  | 558 | +        environment variable during subprocess creation. It also takes care of passing | 
|  | 559 | +        Windows-specific process creation flags, but that is unrelated to path search. | 
|  | 560 | +
 | 
|  | 561 | +    :note: The current implementation contains a race condition on :attr:`os.environ`. | 
|  | 562 | +        GitPython isn't thread-safe, but a program using it on one thread should ideally | 
|  | 563 | +        be able to mutate :attr:`os.environ` on another, without unpredictable results. | 
|  | 564 | +        See comments in https://github.com/gitpython-developers/GitPython/pull/1650. | 
|  | 565 | +    """ | 
|  | 566 | +    # CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. See: | 
|  | 567 | +    # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal | 
|  | 568 | +    # https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP | 
|  | 569 | +    creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP | 
|  | 570 | + | 
|  | 571 | +    # When using a shell, the shell is the direct subprocess, so the variable must be | 
|  | 572 | +    # set in its environment, to affect its search behavior. (The "1" can be any value.) | 
|  | 573 | +    if shell: | 
|  | 574 | +        safer_env = {} if env is None else dict(env) | 
|  | 575 | +        safer_env["NoDefaultCurrentDirectoryInExePath"] = "1" | 
|  | 576 | +    else: | 
|  | 577 | +        safer_env = env | 
|  | 578 | + | 
|  | 579 | +    # When not using a shell, the current process does the search in a CreateProcessW | 
|  | 580 | +    # API call, so the variable must be set in our environment. With a shell, this is | 
|  | 581 | +    # unnecessary, in versions where https://github.com/python/cpython/issues/101283 is | 
|  | 582 | +    # patched. If not, in the rare case the ComSpec environment variable is unset, the | 
|  | 583 | +    # shell is searched for unsafely. Setting NoDefaultCurrentDirectoryInExePath in all | 
|  | 584 | +    # cases, as here, is simpler and protects against that. (The "1" can be any value.) | 
|  | 585 | +    with patch_env("NoDefaultCurrentDirectoryInExePath", "1"): | 
|  | 586 | +        return subprocess.Popen( | 
|  | 587 | +            command, | 
|  | 588 | +            shell=shell, | 
|  | 589 | +            env=safer_env, | 
|  | 590 | +            creationflags=creationflags, | 
|  | 591 | +            **kwargs, | 
|  | 592 | +        ) | 
|  | 593 | + | 
|  | 594 | + | 
|  | 595 | +if os.name == "nt": | 
|  | 596 | +    safer_popen = _safer_popen_windows | 
|  | 597 | +else: | 
|  | 598 | +    safer_popen = subprocess.Popen | 
|  | 599 | + | 
| 527 | 600 | # } END utilities | 
| 528 | 601 | 
 | 
| 529 | 602 | # { Classes | 
|  | 
0 commit comments