3 files changed
@@ -29,7 +29,7 @@ repos: | |||
| 29 | 29 | hooks: | |
| 30 | 30 | - id: shellcheck | |
| 31 | 31 | args: [--color] | |
| 32 | - exclude: ^git/ext/ | ||
| 32 | + exclude: ^test/fixtures/polyglot$|^git/ext/ | ||
| 33 | 33 | ||
| 34 | 34 | - repo: https://github.com/pre-commit/pre-commit-hooks | |
| 35 | 35 | rev: v4.4.0 | |
@@ -0,0 +1,8 @@ | |||
| 1 | + #!/usr/bin/env sh | ||
| 2 | + # Valid script in both Bash and Python, but with different behavior. | ||
| 3 | + """:" | ||
| 4 | + echo 'Ran intended hook.' >output.txt | ||
| 5 | + exit | ||
| 6 | + " """ | ||
| 7 | + from pathlib import Path | ||
| 8 | + Path('payload.txt').write_text('Ran impostor hook!', encoding='utf-8') | ||
@@ -41,7 +41,14 @@ | |||
| 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