Skip to content

Commit d1a631d

Browse files
authored
Merge pull request #2193 from gitpython-developers/more-unsafe-options
Block unsafe Git file and URL options
2 parents ab33e33 + 52199b3 commit d1a631d

7 files changed

Lines changed: 66 additions & 1 deletion

File tree

git/index/base.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
130130
index directly before operating on it using the git command.
131131
"""
132132

133+
unsafe_git_checkout_index_options = ["--prefix"]
134+
133135
__slots__ = ("repo", "version", "entries", "_extension_data", "_file_path")
134136

135137
_VERSION = 2
@@ -1212,6 +1214,7 @@ def checkout(
12121214
paths: Union[None, Iterable[PathLike]] = None,
12131215
force: bool = False,
12141216
fprogress: Callable = lambda *args: None,
1217+
allow_unsafe_options: bool = False,
12151218
**kwargs: Any,
12161219
) -> Union[None, Iterator[PathLike], Sequence[PathLike]]:
12171220
"""Check out the given paths or all files from the version known to the index
@@ -1238,6 +1241,9 @@ def checkout(
12381241
no explicit paths are given. Otherwise progress information will be send
12391242
prior and after a file has been checked out.
12401243
1244+
:param allow_unsafe_options:
1245+
Allow unsafe options, such as ``--prefix``.
1246+
12411247
:param kwargs:
12421248
Additional arguments to be passed to :manpage:`git-checkout-index(1)`.
12431249
@@ -1261,6 +1267,12 @@ def checkout(
12611267
i.e. if you want :manpage:`git-checkout(1)`-like behaviour, use
12621268
``head.checkout`` instead of ``index.checkout``.
12631269
"""
1270+
if not allow_unsafe_options:
1271+
Git.check_unsafe_options(
1272+
options=Git._option_candidates([], kwargs),
1273+
unsafe_options=self.unsafe_git_checkout_index_options,
1274+
)
1275+
12641276
args = ["--index"]
12651277
if force:
12661278
args.append("--force")

git/refs/tag.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from typing import Any, TYPE_CHECKING, Type, Union
1818

19+
from git.cmd import Git
1920
from git.types import AnyGitObject, PathLike
2021

2122
if TYPE_CHECKING:
@@ -42,6 +43,8 @@ class TagReference(Reference):
4243

4344
__slots__ = ()
4445

46+
unsafe_git_tag_options = ["--file", "-F"]
47+
4548
_common_default = "tags"
4649
_common_path_default = Reference._common_path_default + "/" + _common_default
4750

@@ -92,6 +95,7 @@ def create(
9295
reference: Union[str, "SymbolicReference"] = "HEAD",
9396
logmsg: Union[str, None] = None,
9497
force: bool = False,
98+
allow_unsafe_options: bool = False,
9599
**kwargs: Any,
96100
) -> "TagReference":
97101
"""Create a new tag reference.
@@ -121,12 +125,21 @@ def create(
121125
:param force:
122126
If ``True``, force creation of a tag even though that tag already exists.
123127
128+
:param allow_unsafe_options:
129+
Allow unsafe options, such as ``--file``.
130+
124131
:param kwargs:
125132
Additional keyword arguments to be passed to :manpage:`git-tag(1)`.
126133
127134
:return:
128135
A new :class:`TagReference`.
129136
"""
137+
if not allow_unsafe_options:
138+
Git.check_unsafe_options(
139+
options=Git._option_candidates([], kwargs),
140+
unsafe_options=cls.unsafe_git_tag_options,
141+
)
142+
130143
if "ref" in kwargs and kwargs["ref"]:
131144
reference = kwargs["ref"]
132145

git/repo/base.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,10 @@ class Repo:
151151
"-c",
152152
# Can install hooks that execute during clone:
153153
"--template",
154+
# Fetches from an additional caller-controlled URI:
155+
"--bundle-uri",
154156
]
155-
"""Options to :manpage:`git-clone(1)` that allow arbitrary commands to be executed.
157+
"""Options to :manpage:`git-clone(1)` that permit unsafe command execution or I/O.
156158
157159
The ``--upload-pack``/``-u`` option allows users to execute arbitrary commands
158160
directly:
@@ -164,6 +166,11 @@ class Repo:
164166
165167
The ``--template`` option can install hooks that execute during clone:
166168
https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---templatetemplate-directory
169+
170+
The ``--bundle-uri`` option fetches from an additional URI before fetching from the
171+
clone URL. An untrusted value can therefore make Git access local files or
172+
unintended network resources:
173+
https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---bundle-uriuri
167174
"""
168175

169176
unsafe_git_archive_options = [
@@ -172,6 +179,10 @@ class Repo:
172179
# Writes output to a caller-controlled filesystem path.
173180
"--output",
174181
"-o",
182+
# Reads from a caller-controlled filesystem path:
183+
"--add-file",
184+
# Injects a caller-controlled path and contents:
185+
"--add-virtual-file",
175186
]
176187

177188
unsafe_git_revision_options = [

test/test_clone.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ def test_clone_unsafe_options(self, rw_repo):
132132
"-cprotocol.ext.allow=always",
133133
"-vcprotocol.ext.allow=always",
134134
f"--template={tmp_dir}",
135+
f"--bundle-uri=file://{tmp_dir}",
135136
]
136137
for unsafe_option in unsafe_options:
137138
with self.assertRaises(UnsafeOptionError):
@@ -147,6 +148,7 @@ def test_clone_unsafe_options(self, rw_repo):
147148
{"conf": "protocol.ext.allow=always"},
148149
{"c": "protocol.ext.allow=always"},
149150
{"template": tmp_dir},
151+
{"bundle_uri": f"file://{tmp_dir}"},
150152
]
151153
for unsafe_option in unsafe_options:
152154
with self.assertRaises(UnsafeOptionError):

test/test_index.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
HookExecutionError,
3232
InvalidGitRepositoryError,
3333
UnmergedEntriesError,
34+
UnsafeOptionError,
3435
)
3536
from git.index.fun import hook_path, run_commit_hook
3637
from git.index.typ import BaseIndexEntry, IndexEntry
@@ -202,6 +203,15 @@ def _make_hook(git_dir, name, content, make_exec=True):
202203

203204
@ddt.ddt
204205
class TestIndex(TestBase):
206+
@with_rw_repo("HEAD")
207+
def test_checkout_rejects_unsafe_prefix(self, rw_repo):
208+
with tempfile.TemporaryDirectory() as target:
209+
with self.assertRaises(UnsafeOptionError):
210+
rw_repo.index.checkout(prefix=f"{target}/")
211+
212+
rw_repo.index.checkout(prefix=f"{target}/", allow_unsafe_options=True)
213+
self.assertTrue(osp.isfile(osp.join(target, "CHANGES")))
214+
205215
def __init__(self, *args):
206216
super().__init__(*args)
207217
self._reset_progress()

test/test_refs.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
SymbolicReference,
2424
TagReference,
2525
)
26+
from git.exc import UnsafeOptionError
2627
from git.objects.tag import TagObject
2728
import git.refs as refs
2829
from git.util import Actor
@@ -60,6 +61,18 @@ def test_from_path(self):
6061
# Check remoteness
6162
assert Reference(self.rorepo, "refs/remotes/origin").is_remote()
6263

64+
@with_rw_repo("HEAD")
65+
def test_tag_create_rejects_unsafe_file_options(self, rw_repo):
66+
with tempfile.NamedTemporaryFile("w", encoding="utf-8") as message:
67+
message.write("private tag message")
68+
message.flush()
69+
for index, option in enumerate(({"F": message.name}, {"file": message.name})):
70+
with self.assertRaises(UnsafeOptionError):
71+
TagReference.create(rw_repo, f"unsafe-{index}", **option)
72+
73+
tag = TagReference.create(rw_repo, "allowed-file", F=message.name, allow_unsafe_options=True)
74+
self.assertEqual(tag.tag.message, "private tag message")
75+
6376
def test_from_pathlike(self):
6477
# Should be able to create any reference directly.
6578
for ref_type in (Reference, Head, TagReference, RemoteReference):

test/test_repo.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,10 @@ def test_archive_rejects_unsafe_options(self):
433433
with self.assertRaises(UnsafeOptionError):
434434
self.rorepo.archive(io.BytesIO(), "0.1.6", output=output_marker)
435435
assert not osp.exists(output_marker)
436+
with self.assertRaises(UnsafeOptionError):
437+
self.rorepo.archive(io.BytesIO(), "0.1.6", add_file=output_marker)
438+
with self.assertRaises(UnsafeOptionError):
439+
self.rorepo.archive(io.BytesIO(), "0.1.6", add_virtual_file="file:content")
436440

437441
def test_archive_rejects_unsafe_remote_protocol(self):
438442
with tempfile.TemporaryDirectory() as tdir:

0 commit comments

Comments
 (0)