Skip to content

Commit 13cc735

Browse files
codexByron
authored andcommitted
Guard unsafe git init options
GHSA-9rj7-rf2p-w77r reports that Repo.init forwarded git-init options without applying GitPython's unsafe-option policy. A regression showed template and abbreviated option spellings reached Git without an UnsafeOptionError and could create the destination before validation. Add a git-init denylist for template installation and separate Git directory redirection, check keyword options before any path or directory mutation, and provide the standard explicit allow_unsafe_options escape hatch. This preserves trusted uses while rejecting untrusted forwarding by default. An audit against Git cf5497b14c5a24f10c13f7e0ee85cb95af13ea6a (v2.55.0.windows.3-16-gcf5497b14c) confirmed that init and clone are the built-in commands that consume repository template directories. Clone, clone_from, and submodule cloning already share the guarded clone helper; the similarly named commit option only reads a commit-message template. Validated with the focused init regression, the clone/init unsafe-option suite, 185 config/Git/index/clone tests, Ruff, and basedpyright.
1 parent 8ff1b66 commit 13cc735

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

git/repo/base.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ class Repo:
142142
re_author_committer_start = re.compile(r"^(author|committer)")
143143
re_tab_full_line = re.compile(r"^\t(.*)$")
144144

145+
unsafe_git_init_options = [
146+
# Can install hooks that execute during later Git commands:
147+
"--template",
148+
# Redirects the repository metadata to a caller-controlled path:
149+
"--separate-git-dir",
150+
]
151+
"""Options to :manpage:`git-init(1)` that permit unsafe code execution or I/O."""
152+
145153
unsafe_git_clone_options = [
146154
# Executes arbitrary commands:
147155
"--upload-pack",
@@ -1394,6 +1402,7 @@ def init(
13941402
mkdir: bool = True,
13951403
odbt: Type[GitCmdObjectDB] = GitCmdObjectDB,
13961404
expand_vars: bool = True,
1405+
allow_unsafe_options: bool = False,
13971406
**kwargs: Any,
13981407
) -> "Repo":
13991408
"""Initialize a git repository at the given path if specified.
@@ -1418,13 +1427,22 @@ def init(
14181427
information disclosure, allowing attackers to access the contents of
14191428
environment variables.
14201429
1430+
:param allow_unsafe_options:
1431+
Allow unsafe options to be used, such as ``--template`` and
1432+
``--separate-git-dir``.
1433+
14211434
:param kwargs:
14221435
Keyword arguments serving as additional options to the
14231436
:manpage:`git-init(1)` command.
14241437
14251438
:return:
14261439
:class:`Repo` (the newly created repo)
14271440
"""
1441+
if not allow_unsafe_options:
1442+
Git.check_unsafe_options(
1443+
options=Git._option_candidates([], kwargs),
1444+
unsafe_options=cls.unsafe_git_init_options,
1445+
)
14281446
if path:
14291447
path = expand_path(path, expand_vars)
14301448
if mkdir and path and not osp.exists(path):

test/test_repo.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,33 @@ def test_new_should_raise_on_invalid_repo_location(self):
8282
with tempfile.TemporaryDirectory() as tdir:
8383
self.assertRaises(InvalidGitRepositoryError, Repo, tdir)
8484

85+
def test_init_rejects_unsafe_options(self):
86+
with tempfile.TemporaryDirectory() as tdir:
87+
template_dir = osp.join(tdir, "template")
88+
os.mkdir(template_dir)
89+
unsafe_options = [
90+
{"template": template_dir},
91+
{"templa": template_dir},
92+
{"separate_git_dir": osp.join(tdir, "git-dir")},
93+
{"separate_git_di": osp.join(tdir, "git-dir")},
94+
]
95+
for index, kwargs in enumerate(unsafe_options):
96+
repo_dir = osp.join(tdir, f"repo-{index}")
97+
with self.assertRaises(UnsafeOptionError):
98+
Repo.init(repo_dir, **kwargs)
99+
assert not osp.exists(repo_dir)
100+
101+
def test_init_allows_explicitly_unsafe_options(self):
102+
with tempfile.TemporaryDirectory() as tdir:
103+
template_dir = osp.join(tdir, "template")
104+
os.mkdir(template_dir)
105+
repo = Repo.init(
106+
osp.join(tdir, "repo"),
107+
template=template_dir,
108+
allow_unsafe_options=True,
109+
)
110+
assert repo.git_dir
111+
85112
@with_rw_directory
86113
def test_new_should_raise_on_invalid_repo_location_within_repo(self, rw_dir):
87114
repo_dir = osp.join(rw_dir, "repo")

0 commit comments

Comments
 (0)