|
41 | 41 | from git.objects import Blob
|
42 | 42 | from git.util import Actor, cwd, hex_to_bin, rmtree
|
43 | 43 | from gitdb.base import IStream
|
44 |
| -from test.lib import TestBase, fixture, fixture_path, with_rw_directory, with_rw_repo |
| 44 | +from test.lib import ( |
| 45 | + TestBase, |
| 46 | + VirtualEnvironment, |
| 47 | + fixture, |
| 48 | + fixture_path, |
| 49 | + with_rw_directory, |
| 50 | + with_rw_repo, |
| 51 | +) |
45 | 52 |
|
46 | 53 | HOOKS_SHEBANG = "#!/usr/bin/env sh\n"
|
47 | 54 |
|
@@ -1016,36 +1023,46 @@ def test_run_commit_hook(self, rw_repo):
|
1016 | 1023 | output = Path(rw_repo.git_dir, "output.txt").read_text(encoding="utf-8")
|
1017 | 1024 | self.assertEqual(output, "ran fake hook\n")
|
1018 | 1025 |
|
1019 |
| - # FIXME: Figure out a way to make this test also work with Absent and WslNoDistro. |
1020 |
| - @pytest.mark.xfail( |
1021 |
| - type(_win_bash_status) is WinBashStatus.WslNoDistro, |
1022 |
| - reason="Currently uses the bash.exe of WSL, even with no WSL distro installed", |
1023 |
| - raises=HookExecutionError, |
1024 |
| - ) |
1025 | 1026 | @ddt.data((False,), (True,))
|
1026 | 1027 | @with_rw_directory
|
1027 | 1028 | def test_hook_uses_shell_not_from_cwd(self, rw_dir, case):
|
1028 | 1029 | (chdir_to_repo,) = case
|
1029 | 1030 |
|
| 1031 | + shell_name = "bash.exe" if os.name == "nt" else "sh" |
| 1032 | + maybe_chdir = cwd(rw_dir) if chdir_to_repo else contextlib.nullcontext() |
1030 | 1033 | repo = Repo.init(rw_dir)
|
1031 |
| - _make_hook(repo.git_dir, "fake-hook", "echo 'ran fake hook' >output.txt") |
1032 | 1034 |
|
1033 |
| - if os.name == "nt": |
1034 |
| - # Copy an actual binary that is not bash. |
1035 |
| - other_exe_path = Path(os.environ["SystemRoot"], "system32", "hostname.exe") |
1036 |
| - impostor_path = Path(rw_dir, "bash.exe") |
1037 |
| - shutil.copy(other_exe_path, impostor_path) |
| 1035 | + # We need an impostor shell that works on Windows and that can be distinguished |
| 1036 | + # from the real bash.exe. But even if the real bash.exe is absent or unusable, |
| 1037 | + # we should verify that the impostor is not run. So the impostor needs a clear |
| 1038 | + # side effect (unlike in TestGit.test_it_executes_git_not_from_cwd). Popen on |
| 1039 | + # Windows uses CreateProcessW, which disregards PATHEXT; the impostor may need |
| 1040 | + # to be a binary executable to ensure the vulnerability is found if present. No |
| 1041 | + # compiler need exist, shipping a binary in the test suite may target the wrong |
| 1042 | + # architecture, and generating one in a bespoke way may cause virus scanners to |
| 1043 | + # give a false positive. So we use a Bash/Python polyglot for the hook and use |
| 1044 | + # the Python interpreter itself as the bash.exe impostor. But an interpreter |
| 1045 | + # from a venv may not run outside of it, and a global interpreter won't run from |
| 1046 | + # a different location if it was installed from the Microsoft Store. So we make |
| 1047 | + # a new venv in rw_dir and use its interpreter. |
| 1048 | + venv = VirtualEnvironment(rw_dir, with_pip=False) |
| 1049 | + shutil.copy(venv.python, Path(rw_dir, shell_name)) |
| 1050 | + shutil.copy(fixture_path("polyglot"), hook_path("polyglot", repo.git_dir)) |
| 1051 | + payload = Path(rw_dir, "payload.txt") |
| 1052 | + |
| 1053 | + if type(_win_bash_status) in {WinBashStatus.Absent, WinBashStatus.WslNoDistro}: |
| 1054 | + # The real shell can't run, but the impostor should still not be used. |
| 1055 | + with self.assertRaises(HookExecutionError): |
| 1056 | + with maybe_chdir: |
| 1057 | + run_commit_hook("polyglot", repo.index) |
| 1058 | + self.assertFalse(payload.exists()) |
1038 | 1059 | else:
|
1039 |
| - # Create a shell script that doesn't do anything. |
1040 |
| - impostor_path = Path(rw_dir, "sh") |
1041 |
| - impostor_path.write_text("#!/bin/sh\n", encoding="utf-8") |
1042 |
| - os.chmod(impostor_path, 0o755) |
1043 |
| - |
1044 |
| - with cwd(rw_dir) if chdir_to_repo else contextlib.nullcontext(): |
1045 |
| - run_commit_hook("fake-hook", repo.index) |
1046 |
| - |
1047 |
| - output = Path(rw_dir, "output.txt").read_text(encoding="utf-8") |
1048 |
| - self.assertEqual(output, "ran fake hook\n") |
| 1060 | + # The real shell should run, and not the impostor. |
| 1061 | + with maybe_chdir: |
| 1062 | + run_commit_hook("polyglot", repo.index) |
| 1063 | + self.assertFalse(payload.exists()) |
| 1064 | + output = Path(rw_dir, "output.txt").read_text(encoding="utf-8") |
| 1065 | + self.assertEqual(output, "Ran intended hook.\n") |
1049 | 1066 |
|
1050 | 1067 | @pytest.mark.xfail(
|
1051 | 1068 | type(_win_bash_status) is WinBashStatus.Absent,
|
|
0 commit comments