Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 678a8fe

Browse files
authoredDec 29, 2022
Merge pull request #1521 from stsewd/block-insecure-options
Block insecure options and protocols by default
2 parents ae6a6e4 + f4f2658 commit 678a8fe

File tree

10 files changed

+752
-21
lines changed

10 files changed

+752
-21
lines changed
 

‎AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,5 @@ Contributors are:
5050
-Patrick Gerard
5151
-Luke Twist <itsluketwist@gmail.com>
5252
-Joseph Hale <me _at_ jhale.dev>
53+
-Santos Gallegos <stsewd _at_ proton.me>
5354
Portions derived from other open source works and are clearly marked.

‎git/cmd.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# This module is part of GitPython and is released under
55
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
66
from __future__ import annotations
7+
import re
78
from contextlib import contextmanager
89
import io
910
import logging
@@ -24,7 +25,7 @@
2425
from git.exc import CommandError
2526
from git.util import is_cygwin_git, cygpath, expand_path, remove_password_if_present
2627

27-
from .exc import GitCommandError, GitCommandNotFound
28+
from .exc import GitCommandError, GitCommandNotFound, UnsafeOptionError, UnsafeProtocolError
2829
from .util import (
2930
LazyMixin,
3031
stream_copy,
@@ -262,6 +263,8 @@ class Git(LazyMixin):
262263

263264
_excluded_ = ("cat_file_all", "cat_file_header", "_version_info")
264265

266+
re_unsafe_protocol = re.compile("(.+)::.+")
267+
265268
def __getstate__(self) -> Dict[str, Any]:
266269
return slots_to_dict(self, exclude=self._excluded_)
267270

@@ -454,6 +457,48 @@ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike:
454457
url = url.replace("\\\\", "\\").replace("\\", "/")
455458
return url
456459

460+
@classmethod
461+
def check_unsafe_protocols(cls, url: str) -> None:
462+
"""
463+
Check for unsafe protocols.
464+
465+
Apart from the usual protocols (http, git, ssh),
466+
Git allows "remote helpers" that have the form `<transport>::<address>`,
467+
one of these helpers (`ext::`) can be used to invoke any arbitrary command.
468+
469+
See:
470+
471+
- https://git-scm.com/docs/gitremote-helpers
472+
- https://git-scm.com/docs/git-remote-ext
473+
"""
474+
match = cls.re_unsafe_protocol.match(url)
475+
if match:
476+
protocol = match.group(1)
477+
raise UnsafeProtocolError(
478+
f"The `{protocol}::` protocol looks suspicious, use `allow_unsafe_protocols=True` to allow it."
479+
)
480+
481+
@classmethod
482+
def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None:
483+
"""
484+
Check for unsafe options.
485+
486+
Some options that are passed to `git <command>` can be used to execute
487+
arbitrary commands, this are blocked by default.
488+
"""
489+
# Options can be of the form `foo` or `--foo bar` `--foo=bar`,
490+
# so we need to check if they start with "--foo" or if they are equal to "foo".
491+
bare_unsafe_options = [
492+
option.lstrip("-")
493+
for option in unsafe_options
494+
]
495+
for option in options:
496+
for unsafe_option, bare_option in zip(unsafe_options, bare_unsafe_options):
497+
if option.startswith(unsafe_option) or option == bare_option:
498+
raise UnsafeOptionError(
499+
f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
500+
)
501+
457502
class AutoInterrupt(object):
458503
"""Kill/Interrupt the stored process instance once this instance goes out of scope. It is
459504
used to prevent processes piling up in case iterators stop reading.
@@ -1148,12 +1193,12 @@ def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any
11481193
return args
11491194

11501195
@classmethod
1151-
def __unpack_args(cls, arg_list: Sequence[str]) -> List[str]:
1196+
def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]:
11521197

11531198
outlist = []
11541199
if isinstance(arg_list, (list, tuple)):
11551200
for arg in arg_list:
1156-
outlist.extend(cls.__unpack_args(arg))
1201+
outlist.extend(cls._unpack_args(arg))
11571202
else:
11581203
outlist.append(str(arg_list))
11591204

@@ -1238,7 +1283,7 @@ def _call_process(
12381283
# Prepare the argument list
12391284

12401285
opt_args = self.transform_kwargs(**opts_kwargs)
1241-
ext_args = self.__unpack_args([a for a in args if a is not None])
1286+
ext_args = self._unpack_args([a for a in args if a is not None])
12421287

12431288
if insert_after_this_arg is None:
12441289
args_list = opt_args + ext_args

‎git/exc.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ class NoSuchPathError(GitError, OSError):
3737
"""Thrown if a path could not be access by the system."""
3838

3939

40+
class UnsafeProtocolError(GitError):
41+
"""Thrown if unsafe protocols are passed without being explicitly allowed."""
42+
43+
44+
class UnsafeOptionError(GitError):
45+
"""Thrown if unsafe options are passed without being explicitly allowed."""
46+
47+
4048
class CommandError(GitError):
4149
"""Base class for exceptions thrown at every stage of `Popen()` execution.
4250

‎git/objects/submodule/base.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,16 @@ def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> Path
272272
# end
273273

274274
@classmethod
275-
def _clone_repo(cls, repo: "Repo", url: str, path: PathLike, name: str, **kwargs: Any) -> "Repo":
275+
def _clone_repo(
276+
cls,
277+
repo: "Repo",
278+
url: str,
279+
path: PathLike,
280+
name: str,
281+
allow_unsafe_options: bool = False,
282+
allow_unsafe_protocols: bool = False,
283+
**kwargs: Any,
284+
) -> "Repo":
276285
""":return: Repo instance of newly cloned repository
277286
:param repo: our parent repository
278287
:param url: url to clone from
@@ -289,7 +298,13 @@ def _clone_repo(cls, repo: "Repo", url: str, path: PathLike, name: str, **kwargs
289298
module_checkout_path = osp.join(str(repo.working_tree_dir), path)
290299
# end
291300

292-
clone = git.Repo.clone_from(url, module_checkout_path, **kwargs)
301+
clone = git.Repo.clone_from(
302+
url,
303+
module_checkout_path,
304+
allow_unsafe_options=allow_unsafe_options,
305+
allow_unsafe_protocols=allow_unsafe_protocols,
306+
**kwargs,
307+
)
293308
if cls._need_gitfile_submodules(repo.git):
294309
cls._write_git_file_and_module_config(module_checkout_path, module_abspath)
295310
# end
@@ -359,6 +374,8 @@ def add(
359374
depth: Union[int, None] = None,
360375
env: Union[Mapping[str, str], None] = None,
361376
clone_multi_options: Union[Sequence[TBD], None] = None,
377+
allow_unsafe_options: bool = False,
378+
allow_unsafe_protocols: bool = False,
362379
) -> "Submodule":
363380
"""Add a new submodule to the given repository. This will alter the index
364381
as well as the .gitmodules file, but will not create a new commit.
@@ -475,7 +492,16 @@ def add(
475492
kwargs["multi_options"] = clone_multi_options
476493

477494
# _clone_repo(cls, repo, url, path, name, **kwargs):
478-
mrepo = cls._clone_repo(repo, url, path, name, env=env, **kwargs)
495+
mrepo = cls._clone_repo(
496+
repo,
497+
url,
498+
path,
499+
name,
500+
env=env,
501+
allow_unsafe_options=allow_unsafe_options,
502+
allow_unsafe_protocols=allow_unsafe_protocols,
503+
**kwargs,
504+
)
479505
# END verify url
480506

481507
## See #525 for ensuring git urls in config-files valid under Windows.
@@ -520,6 +546,8 @@ def update(
520546
keep_going: bool = False,
521547
env: Union[Mapping[str, str], None] = None,
522548
clone_multi_options: Union[Sequence[TBD], None] = None,
549+
allow_unsafe_options: bool = False,
550+
allow_unsafe_protocols: bool = False,
523551
) -> "Submodule":
524552
"""Update the repository of this submodule to point to the checkout
525553
we point at with the binsha of this instance.
@@ -643,6 +671,8 @@ def update(
643671
n=True,
644672
env=env,
645673
multi_options=clone_multi_options,
674+
allow_unsafe_options=allow_unsafe_options,
675+
allow_unsafe_protocols=allow_unsafe_protocols,
646676
)
647677
# END handle dry-run
648678
progress.update(

‎git/remote.py

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,23 @@ class Remote(LazyMixin, IterableObj):
539539
__slots__ = ("repo", "name", "_config_reader")
540540
_id_attribute_ = "name"
541541

542+
unsafe_git_fetch_options = [
543+
# This option allows users to execute arbitrary commands.
544+
# https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt---upload-packltupload-packgt
545+
"--upload-pack",
546+
]
547+
unsafe_git_pull_options = [
548+
# This option allows users to execute arbitrary commands.
549+
# https://git-scm.com/docs/git-pull#Documentation/git-pull.txt---upload-packltupload-packgt
550+
"--upload-pack"
551+
]
552+
unsafe_git_push_options = [
553+
# This option allows users to execute arbitrary commands.
554+
# https://git-scm.com/docs/git-push#Documentation/git-push.txt---execltgit-receive-packgt
555+
"--receive-pack",
556+
"--exec",
557+
]
558+
542559
def __init__(self, repo: "Repo", name: str) -> None:
543560
"""Initialize a remote instance
544561
@@ -615,7 +632,9 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator["Remote
615632
yield Remote(repo, section[lbound + 1 : rbound])
616633
# END for each configuration section
617634

618-
def set_url(self, new_url: str, old_url: Optional[str] = None, **kwargs: Any) -> "Remote":
635+
def set_url(
636+
self, new_url: str, old_url: Optional[str] = None, allow_unsafe_protocols: bool = False, **kwargs: Any
637+
) -> "Remote":
619638
"""Configure URLs on current remote (cf command git remote set_url)
620639
621640
This command manages URLs on the remote.
@@ -624,15 +643,17 @@ def set_url(self, new_url: str, old_url: Optional[str] = None, **kwargs: Any) ->
624643
:param old_url: when set, replaces this URL with new_url for the remote
625644
:return: self
626645
"""
646+
if not allow_unsafe_protocols:
647+
Git.check_unsafe_protocols(new_url)
627648
scmd = "set-url"
628649
kwargs["insert_kwargs_after"] = scmd
629650
if old_url:
630-
self.repo.git.remote(scmd, self.name, new_url, old_url, **kwargs)
651+
self.repo.git.remote(scmd, "--", self.name, new_url, old_url, **kwargs)
631652
else:
632-
self.repo.git.remote(scmd, self.name, new_url, **kwargs)
653+
self.repo.git.remote(scmd, "--", self.name, new_url, **kwargs)
633654
return self
634655

635-
def add_url(self, url: str, **kwargs: Any) -> "Remote":
656+
def add_url(self, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote":
636657
"""Adds a new url on current remote (special case of git remote set_url)
637658
638659
This command adds new URLs to a given remote, making it possible to have
@@ -641,7 +662,7 @@ def add_url(self, url: str, **kwargs: Any) -> "Remote":
641662
:param url: string being the URL to add as an extra remote URL
642663
:return: self
643664
"""
644-
return self.set_url(url, add=True)
665+
return self.set_url(url, add=True, allow_unsafe_protocols=allow_unsafe_protocols)
645666

646667
def delete_url(self, url: str, **kwargs: Any) -> "Remote":
647668
"""Deletes a new url on current remote (special case of git remote set_url)
@@ -733,7 +754,7 @@ def stale_refs(self) -> IterableList[Reference]:
733754
return out_refs
734755

735756
@classmethod
736-
def create(cls, repo: "Repo", name: str, url: str, **kwargs: Any) -> "Remote":
757+
def create(cls, repo: "Repo", name: str, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote":
737758
"""Create a new remote to the given repository
738759
:param repo: Repository instance that is to receive the new remote
739760
:param name: Desired name of the remote
@@ -743,7 +764,10 @@ def create(cls, repo: "Repo", name: str, url: str, **kwargs: Any) -> "Remote":
743764
:raise GitCommandError: in case an origin with that name already exists"""
744765
scmd = "add"
745766
kwargs["insert_kwargs_after"] = scmd
746-
repo.git.remote(scmd, name, Git.polish_url(url), **kwargs)
767+
url = Git.polish_url(url)
768+
if not allow_unsafe_protocols:
769+
Git.check_unsafe_protocols(url)
770+
repo.git.remote(scmd, "--", name, url, **kwargs)
747771
return cls(repo, name)
748772

749773
# add is an alias
@@ -925,6 +949,8 @@ def fetch(
925949
progress: Union[RemoteProgress, None, "UpdateProgress"] = None,
926950
verbose: bool = True,
927951
kill_after_timeout: Union[None, float] = None,
952+
allow_unsafe_protocols: bool = False,
953+
allow_unsafe_options: bool = False,
928954
**kwargs: Any,
929955
) -> IterableList[FetchInfo]:
930956
"""Fetch the latest changes for this remote
@@ -967,6 +993,14 @@ def fetch(
967993
else:
968994
args = [refspec]
969995

996+
if not allow_unsafe_protocols:
997+
for ref in args:
998+
if ref:
999+
Git.check_unsafe_protocols(ref)
1000+
1001+
if not allow_unsafe_options:
1002+
Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_fetch_options)
1003+
9701004
proc = self.repo.git.fetch(
9711005
"--", self, *args, as_process=True, with_stdout=False, universal_newlines=True, v=verbose, **kwargs
9721006
)
@@ -980,6 +1014,8 @@ def pull(
9801014
refspec: Union[str, List[str], None] = None,
9811015
progress: Union[RemoteProgress, "UpdateProgress", None] = None,
9821016
kill_after_timeout: Union[None, float] = None,
1017+
allow_unsafe_protocols: bool = False,
1018+
allow_unsafe_options: bool = False,
9831019
**kwargs: Any,
9841020
) -> IterableList[FetchInfo]:
9851021
"""Pull changes from the given branch, being the same as a fetch followed
@@ -994,6 +1030,15 @@ def pull(
9941030
# No argument refspec, then ensure the repo's config has a fetch refspec.
9951031
self._assert_refspec()
9961032
kwargs = add_progress(kwargs, self.repo.git, progress)
1033+
1034+
refspec = Git._unpack_args(refspec or [])
1035+
if not allow_unsafe_protocols:
1036+
for ref in refspec:
1037+
Git.check_unsafe_protocols(ref)
1038+
1039+
if not allow_unsafe_options:
1040+
Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_pull_options)
1041+
9971042
proc = self.repo.git.pull(
9981043
"--", self, refspec, with_stdout=False, as_process=True, universal_newlines=True, v=True, **kwargs
9991044
)
@@ -1007,6 +1052,8 @@ def push(
10071052
refspec: Union[str, List[str], None] = None,
10081053
progress: Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None] = None,
10091054
kill_after_timeout: Union[None, float] = None,
1055+
allow_unsafe_protocols: bool = False,
1056+
allow_unsafe_options: bool = False,
10101057
**kwargs: Any,
10111058
) -> PushInfoList:
10121059
"""Push changes from source branch in refspec to target branch in refspec.
@@ -1037,6 +1084,15 @@ def push(
10371084
be 0.
10381085
Call ``.raise_if_error()`` on the returned object to raise on any failure."""
10391086
kwargs = add_progress(kwargs, self.repo.git, progress)
1087+
1088+
refspec = Git._unpack_args(refspec or [])
1089+
if not allow_unsafe_protocols:
1090+
for ref in refspec:
1091+
Git.check_unsafe_protocols(ref)
1092+
1093+
if not allow_unsafe_options:
1094+
Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_push_options)
1095+
10401096
proc = self.repo.git.push(
10411097
"--",
10421098
self,

‎git/repo/base.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
)
2222
from git.config import GitConfigParser
2323
from git.db import GitCmdObjectDB
24-
from git.exc import InvalidGitRepositoryError, NoSuchPathError, GitCommandError
24+
from git.exc import (
25+
GitCommandError,
26+
InvalidGitRepositoryError,
27+
NoSuchPathError,
28+
)
2529
from git.index import IndexFile
2630
from git.objects import Submodule, RootModule, Commit
2731
from git.refs import HEAD, Head, Reference, TagReference
@@ -129,6 +133,18 @@ class Repo(object):
129133
re_author_committer_start = re.compile(r"^(author|committer)")
130134
re_tab_full_line = re.compile(r"^\t(.*)$")
131135

136+
unsafe_git_clone_options = [
137+
# This option allows users to execute arbitrary commands.
138+
# https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---upload-packltupload-packgt
139+
"--upload-pack",
140+
"-u",
141+
# Users can override configuration variables
142+
# like `protocol.allow` or `core.gitProxy` to execute arbitrary commands.
143+
# https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---configltkeygtltvaluegt
144+
"--config",
145+
"-c",
146+
]
147+
132148
# invariants
133149
# represents the configuration level of a configuration file
134150
config_level: ConfigLevels_Tup = ("system", "user", "global", "repository")
@@ -955,7 +971,7 @@ def blame(
955971
file: str,
956972
incremental: bool = False,
957973
rev_opts: Optional[List[str]] = None,
958-
**kwargs: Any
974+
**kwargs: Any,
959975
) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None:
960976
"""The blame information for the given file at the given revision.
961977
@@ -1146,6 +1162,8 @@ def _clone(
11461162
odb_default_type: Type[GitCmdObjectDB],
11471163
progress: Union["RemoteProgress", "UpdateProgress", Callable[..., "RemoteProgress"], None] = None,
11481164
multi_options: Optional[List[str]] = None,
1165+
allow_unsafe_protocols: bool = False,
1166+
allow_unsafe_options: bool = False,
11491167
**kwargs: Any,
11501168
) -> "Repo":
11511169
odbt = kwargs.pop("odbt", odb_default_type)
@@ -1167,6 +1185,12 @@ def _clone(
11671185
multi = None
11681186
if multi_options:
11691187
multi = shlex.split(" ".join(multi_options))
1188+
1189+
if not allow_unsafe_protocols:
1190+
Git.check_unsafe_protocols(str(url))
1191+
if not allow_unsafe_options and multi_options:
1192+
Git.check_unsafe_options(options=multi_options, unsafe_options=cls.unsafe_git_clone_options)
1193+
11701194
proc = git.clone(
11711195
multi,
11721196
"--",
@@ -1220,6 +1244,8 @@ def clone(
12201244
path: PathLike,
12211245
progress: Optional[Callable] = None,
12221246
multi_options: Optional[List[str]] = None,
1247+
allow_unsafe_protocols: bool = False,
1248+
allow_unsafe_options: bool = False,
12231249
**kwargs: Any,
12241250
) -> "Repo":
12251251
"""Create a clone from this repository.
@@ -1230,6 +1256,7 @@ def clone(
12301256
option per list item which is passed exactly as specified to clone.
12311257
For example ['--config core.filemode=false', '--config core.ignorecase',
12321258
'--recurse-submodule=repo1_path', '--recurse-submodule=repo2_path']
1259+
:param unsafe_protocols: Allow unsafe protocols to be used, like ext
12331260
:param kwargs:
12341261
* odbt = ObjectDatabase Type, allowing to determine the object database
12351262
implementation used by the returned Repo instance
@@ -1243,6 +1270,8 @@ def clone(
12431270
type(self.odb),
12441271
progress,
12451272
multi_options,
1273+
allow_unsafe_protocols=allow_unsafe_protocols,
1274+
allow_unsafe_options=allow_unsafe_options,
12461275
**kwargs,
12471276
)
12481277

@@ -1254,6 +1283,8 @@ def clone_from(
12541283
progress: Optional[Callable] = None,
12551284
env: Optional[Mapping[str, str]] = None,
12561285
multi_options: Optional[List[str]] = None,
1286+
allow_unsafe_protocols: bool = False,
1287+
allow_unsafe_options: bool = False,
12571288
**kwargs: Any,
12581289
) -> "Repo":
12591290
"""Create a clone from the given URL
@@ -1268,12 +1299,23 @@ def clone_from(
12681299
If you want to unset some variable, consider providing empty string
12691300
as its value.
12701301
:param multi_options: See ``clone`` method
1302+
:param unsafe_protocols: Allow unsafe protocols to be used, like ext
12711303
:param kwargs: see the ``clone`` method
12721304
:return: Repo instance pointing to the cloned directory"""
12731305
git = cls.GitCommandWrapperType(os.getcwd())
12741306
if env is not None:
12751307
git.update_environment(**env)
1276-
return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs)
1308+
return cls._clone(
1309+
git,
1310+
url,
1311+
to_path,
1312+
GitCmdObjectDB,
1313+
progress,
1314+
multi_options,
1315+
allow_unsafe_protocols=allow_unsafe_protocols,
1316+
allow_unsafe_options=allow_unsafe_options,
1317+
**kwargs,
1318+
)
12771319

12781320
def archive(
12791321
self,

‎test/test_git.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ def test_call_process_calls_execute(self, git):
3939
self.assertEqual(git.call_args, ((["git", "version"],), {}))
4040

4141
def test_call_unpack_args_unicode(self):
42-
args = Git._Git__unpack_args("Unicode€™")
42+
args = Git._unpack_args("Unicode€™")
4343
mangled_value = "Unicode\u20ac\u2122"
4444
self.assertEqual(args, [mangled_value])
4545

4646
def test_call_unpack_args(self):
47-
args = Git._Git__unpack_args(["git", "log", "--", "Unicode€™"])
47+
args = Git._unpack_args(["git", "log", "--", "Unicode€™"])
4848
mangled_value = "Unicode\u20ac\u2122"
4949
self.assertEqual(args, ["git", "log", "--", mangled_value])
5050

‎test/test_remote.py

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
GitCommandError,
2424
)
2525
from git.cmd import Git
26+
from pathlib import Path
27+
from git.exc import UnsafeOptionError, UnsafeProtocolError
2628
from test.lib import (
2729
TestBase,
2830
with_rw_repo,
@@ -690,6 +692,262 @@ def test_push_error(self, repo):
690692
with self.assertRaisesRegex(GitCommandError, "src refspec __BAD_REF__ does not match any"):
691693
rem.push("__BAD_REF__")
692694

695+
@with_rw_repo("HEAD")
696+
def test_set_unsafe_url(self, rw_repo):
697+
tmp_dir = Path(tempfile.mkdtemp())
698+
tmp_file = tmp_dir / "pwn"
699+
remote = rw_repo.remote("origin")
700+
urls = [
701+
f"ext::sh -c touch% {tmp_file}",
702+
"fd::17/foo",
703+
]
704+
for url in urls:
705+
with self.assertRaises(UnsafeProtocolError):
706+
remote.set_url(url)
707+
assert not tmp_file.exists()
708+
709+
@with_rw_repo("HEAD")
710+
def test_set_unsafe_url_allowed(self, rw_repo):
711+
tmp_dir = Path(tempfile.mkdtemp())
712+
tmp_file = tmp_dir / "pwn"
713+
remote = rw_repo.remote("origin")
714+
urls = [
715+
f"ext::sh -c touch% {tmp_file}",
716+
"fd::17/foo",
717+
]
718+
for url in urls:
719+
remote.set_url(url, allow_unsafe_protocols=True)
720+
assert list(remote.urls)[-1] == url
721+
assert not tmp_file.exists()
722+
723+
@with_rw_repo("HEAD")
724+
def test_add_unsafe_url(self, rw_repo):
725+
tmp_dir = Path(tempfile.mkdtemp())
726+
tmp_file = tmp_dir / "pwn"
727+
remote = rw_repo.remote("origin")
728+
urls = [
729+
f"ext::sh -c touch% {tmp_file}",
730+
"fd::17/foo",
731+
]
732+
for url in urls:
733+
with self.assertRaises(UnsafeProtocolError):
734+
remote.add_url(url)
735+
assert not tmp_file.exists()
736+
737+
@with_rw_repo("HEAD")
738+
def test_add_unsafe_url_allowed(self, rw_repo):
739+
tmp_dir = Path(tempfile.mkdtemp())
740+
tmp_file = tmp_dir / "pwn"
741+
remote = rw_repo.remote("origin")
742+
urls = [
743+
f"ext::sh -c touch% {tmp_file}",
744+
"fd::17/foo",
745+
]
746+
for url in urls:
747+
remote.add_url(url, allow_unsafe_protocols=True)
748+
assert list(remote.urls)[-1] == url
749+
assert not tmp_file.exists()
750+
751+
@with_rw_repo("HEAD")
752+
def test_create_remote_unsafe_url(self, rw_repo):
753+
tmp_dir = Path(tempfile.mkdtemp())
754+
tmp_file = tmp_dir / "pwn"
755+
urls = [
756+
f"ext::sh -c touch% {tmp_file}",
757+
"fd::17/foo",
758+
]
759+
for url in urls:
760+
with self.assertRaises(UnsafeProtocolError):
761+
Remote.create(rw_repo, "origin", url)
762+
assert not tmp_file.exists()
763+
764+
@with_rw_repo("HEAD")
765+
def test_create_remote_unsafe_url_allowed(self, rw_repo):
766+
tmp_dir = Path(tempfile.mkdtemp())
767+
tmp_file = tmp_dir / "pwn"
768+
urls = [
769+
f"ext::sh -c touch% {tmp_file}",
770+
"fd::17/foo",
771+
]
772+
for i, url in enumerate(urls):
773+
remote = Remote.create(rw_repo, f"origin{i}", url, allow_unsafe_protocols=True)
774+
assert remote.url == url
775+
assert not tmp_file.exists()
776+
777+
@with_rw_repo("HEAD")
778+
def test_fetch_unsafe_url(self, rw_repo):
779+
tmp_dir = Path(tempfile.mkdtemp())
780+
tmp_file = tmp_dir / "pwn"
781+
remote = rw_repo.remote("origin")
782+
urls = [
783+
f"ext::sh -c touch% {tmp_file}",
784+
"fd::17/foo",
785+
]
786+
for url in urls:
787+
with self.assertRaises(UnsafeProtocolError):
788+
remote.fetch(url)
789+
assert not tmp_file.exists()
790+
791+
@with_rw_repo("HEAD")
792+
def test_fetch_unsafe_url_allowed(self, rw_repo):
793+
tmp_dir = Path(tempfile.mkdtemp())
794+
tmp_file = tmp_dir / "pwn"
795+
remote = rw_repo.remote("origin")
796+
urls = [
797+
f"ext::sh -c touch% {tmp_file}",
798+
"fd::17/foo",
799+
]
800+
for url in urls:
801+
# The URL will be allowed into the command, but the command will
802+
# fail since we don't have that protocol enabled in the Git config file.
803+
with self.assertRaises(GitCommandError):
804+
remote.fetch(url, allow_unsafe_protocols=True)
805+
assert not tmp_file.exists()
806+
807+
@with_rw_repo("HEAD")
808+
def test_fetch_unsafe_options(self, rw_repo):
809+
remote = rw_repo.remote("origin")
810+
tmp_dir = Path(tempfile.mkdtemp())
811+
tmp_file = tmp_dir / "pwn"
812+
unsafe_options = [{"upload-pack": f"touch {tmp_file}"}]
813+
for unsafe_option in unsafe_options:
814+
with self.assertRaises(UnsafeOptionError):
815+
remote.fetch(**unsafe_option)
816+
assert not tmp_file.exists()
817+
818+
@with_rw_repo("HEAD")
819+
def test_fetch_unsafe_options_allowed(self, rw_repo):
820+
remote = rw_repo.remote("origin")
821+
tmp_dir = Path(tempfile.mkdtemp())
822+
tmp_file = tmp_dir / "pwn"
823+
unsafe_options = [{"upload-pack": f"touch {tmp_file}"}]
824+
for unsafe_option in unsafe_options:
825+
# The options will be allowed, but the command will fail.
826+
assert not tmp_file.exists()
827+
with self.assertRaises(GitCommandError):
828+
remote.fetch(**unsafe_option, allow_unsafe_options=True)
829+
assert tmp_file.exists()
830+
831+
@with_rw_repo("HEAD")
832+
def test_pull_unsafe_url(self, rw_repo):
833+
tmp_dir = Path(tempfile.mkdtemp())
834+
tmp_file = tmp_dir / "pwn"
835+
remote = rw_repo.remote("origin")
836+
urls = [
837+
f"ext::sh -c touch% {tmp_file}",
838+
"fd::17/foo",
839+
]
840+
for url in urls:
841+
with self.assertRaises(UnsafeProtocolError):
842+
remote.pull(url)
843+
assert not tmp_file.exists()
844+
845+
@with_rw_repo("HEAD")
846+
def test_pull_unsafe_url_allowed(self, rw_repo):
847+
tmp_dir = Path(tempfile.mkdtemp())
848+
tmp_file = tmp_dir / "pwn"
849+
remote = rw_repo.remote("origin")
850+
urls = [
851+
f"ext::sh -c touch% {tmp_file}",
852+
"fd::17/foo",
853+
]
854+
for url in urls:
855+
# The URL will be allowed into the command, but the command will
856+
# fail since we don't have that protocol enabled in the Git config file.
857+
with self.assertRaises(GitCommandError):
858+
remote.pull(url, allow_unsafe_protocols=True)
859+
assert not tmp_file.exists()
860+
861+
@with_rw_repo("HEAD")
862+
def test_pull_unsafe_options(self, rw_repo):
863+
remote = rw_repo.remote("origin")
864+
tmp_dir = Path(tempfile.mkdtemp())
865+
tmp_file = tmp_dir / "pwn"
866+
unsafe_options = [{"upload-pack": f"touch {tmp_file}"}]
867+
for unsafe_option in unsafe_options:
868+
with self.assertRaises(UnsafeOptionError):
869+
remote.pull(**unsafe_option)
870+
assert not tmp_file.exists()
871+
872+
@with_rw_repo("HEAD")
873+
def test_pull_unsafe_options_allowed(self, rw_repo):
874+
remote = rw_repo.remote("origin")
875+
tmp_dir = Path(tempfile.mkdtemp())
876+
tmp_file = tmp_dir / "pwn"
877+
unsafe_options = [{"upload-pack": f"touch {tmp_file}"}]
878+
for unsafe_option in unsafe_options:
879+
# The options will be allowed, but the command will fail.
880+
assert not tmp_file.exists()
881+
with self.assertRaises(GitCommandError):
882+
remote.pull(**unsafe_option, allow_unsafe_options=True)
883+
assert tmp_file.exists()
884+
885+
@with_rw_repo("HEAD")
886+
def test_push_unsafe_url(self, rw_repo):
887+
tmp_dir = Path(tempfile.mkdtemp())
888+
tmp_file = tmp_dir / "pwn"
889+
remote = rw_repo.remote("origin")
890+
urls = [
891+
f"ext::sh -c touch% {tmp_file}",
892+
"fd::17/foo",
893+
]
894+
for url in urls:
895+
with self.assertRaises(UnsafeProtocolError):
896+
remote.push(url)
897+
assert not tmp_file.exists()
898+
899+
@with_rw_repo("HEAD")
900+
def test_push_unsafe_url_allowed(self, rw_repo):
901+
tmp_dir = Path(tempfile.mkdtemp())
902+
tmp_file = tmp_dir / "pwn"
903+
remote = rw_repo.remote("origin")
904+
urls = [
905+
f"ext::sh -c touch% {tmp_file}",
906+
"fd::17/foo",
907+
]
908+
for url in urls:
909+
# The URL will be allowed into the command, but the command will
910+
# fail since we don't have that protocol enabled in the Git config file.
911+
with self.assertRaises(GitCommandError):
912+
remote.push(url, allow_unsafe_protocols=True)
913+
assert not tmp_file.exists()
914+
915+
@with_rw_repo("HEAD")
916+
def test_push_unsafe_options(self, rw_repo):
917+
remote = rw_repo.remote("origin")
918+
tmp_dir = Path(tempfile.mkdtemp())
919+
tmp_file = tmp_dir / "pwn"
920+
unsafe_options = [
921+
{
922+
"receive-pack": f"touch {tmp_file}",
923+
"exec": f"touch {tmp_file}",
924+
}
925+
]
926+
for unsafe_option in unsafe_options:
927+
assert not tmp_file.exists()
928+
with self.assertRaises(UnsafeOptionError):
929+
remote.push(**unsafe_option)
930+
assert not tmp_file.exists()
931+
932+
@with_rw_repo("HEAD")
933+
def test_push_unsafe_options_allowed(self, rw_repo):
934+
remote = rw_repo.remote("origin")
935+
tmp_dir = Path(tempfile.mkdtemp())
936+
tmp_file = tmp_dir / "pwn"
937+
unsafe_options = [
938+
{
939+
"receive-pack": f"touch {tmp_file}",
940+
"exec": f"touch {tmp_file}",
941+
}
942+
]
943+
for unsafe_option in unsafe_options:
944+
# The options will be allowed, but the command will fail.
945+
assert not tmp_file.exists()
946+
with self.assertRaises(GitCommandError):
947+
remote.push(**unsafe_option, allow_unsafe_options=True)
948+
assert tmp_file.exists()
949+
tmp_file.unlink()
950+
693951

694952
class TestTimeouts(TestBase):
695953
@with_rw_repo("HEAD", bare=False)

‎test/test_repo.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
)
3838
from git.exc import (
3939
BadObject,
40+
UnsafeOptionError,
41+
UnsafeProtocolError,
4042
)
4143
from git.repo.fun import touch
4244
from test.lib import TestBase, with_rw_repo, fixture
@@ -223,6 +225,7 @@ def test_clone_from_pathlib_withConfig(self, rw_dir):
223225
"--config submodule.repo.update=checkout",
224226
"--config filter.lfs.clean='git-lfs clean -- %f'",
225227
],
228+
allow_unsafe_options=True,
226229
)
227230

228231
self.assertEqual(cloned.config_reader().get_value("submodule", "active"), "repo")
@@ -263,6 +266,146 @@ def test_leaking_password_in_clone_logs(self, rw_dir):
263266
to_path=rw_dir,
264267
)
265268

269+
@with_rw_repo("HEAD")
270+
def test_clone_unsafe_options(self, rw_repo):
271+
tmp_dir = pathlib.Path(tempfile.mkdtemp())
272+
tmp_file = tmp_dir / "pwn"
273+
unsafe_options = [
274+
f"--upload-pack='touch {tmp_file}'",
275+
f"-u 'touch {tmp_file}'",
276+
"--config=protocol.ext.allow=always",
277+
"-c protocol.ext.allow=always",
278+
]
279+
for unsafe_option in unsafe_options:
280+
with self.assertRaises(UnsafeOptionError):
281+
rw_repo.clone(tmp_dir, multi_options=[unsafe_option])
282+
assert not tmp_file.exists()
283+
284+
@with_rw_repo("HEAD")
285+
def test_clone_unsafe_options_allowed(self, rw_repo):
286+
tmp_dir = pathlib.Path(tempfile.mkdtemp())
287+
tmp_file = tmp_dir / "pwn"
288+
unsafe_options = [
289+
f"--upload-pack='touch {tmp_file}'",
290+
f"-u 'touch {tmp_file}'",
291+
]
292+
for i, unsafe_option in enumerate(unsafe_options):
293+
destination = tmp_dir / str(i)
294+
assert not tmp_file.exists()
295+
# The options will be allowed, but the command will fail.
296+
with self.assertRaises(GitCommandError):
297+
rw_repo.clone(destination, multi_options=[unsafe_option], allow_unsafe_options=True)
298+
assert tmp_file.exists()
299+
tmp_file.unlink()
300+
301+
unsafe_options = [
302+
"--config=protocol.ext.allow=always",
303+
"-c protocol.ext.allow=always",
304+
]
305+
for i, unsafe_option in enumerate(unsafe_options):
306+
destination = tmp_dir / str(i)
307+
assert not destination.exists()
308+
rw_repo.clone(destination, multi_options=[unsafe_option], allow_unsafe_options=True)
309+
assert destination.exists()
310+
311+
@with_rw_repo("HEAD")
312+
def test_clone_safe_options(self, rw_repo):
313+
tmp_dir = pathlib.Path(tempfile.mkdtemp())
314+
options = [
315+
"--depth=1",
316+
"--single-branch",
317+
"-q",
318+
]
319+
for option in options:
320+
destination = tmp_dir / option
321+
assert not destination.exists()
322+
rw_repo.clone(destination, multi_options=[option])
323+
assert destination.exists()
324+
325+
@with_rw_repo("HEAD")
326+
def test_clone_from_unsafe_options(self, rw_repo):
327+
tmp_dir = pathlib.Path(tempfile.mkdtemp())
328+
tmp_file = tmp_dir / "pwn"
329+
unsafe_options = [
330+
f"--upload-pack='touch {tmp_file}'",
331+
f"-u 'touch {tmp_file}'",
332+
"--config=protocol.ext.allow=always",
333+
"-c protocol.ext.allow=always",
334+
]
335+
for unsafe_option in unsafe_options:
336+
with self.assertRaises(UnsafeOptionError):
337+
Repo.clone_from(rw_repo.working_dir, tmp_dir, multi_options=[unsafe_option])
338+
assert not tmp_file.exists()
339+
340+
@with_rw_repo("HEAD")
341+
def test_clone_from_unsafe_options_allowed(self, rw_repo):
342+
tmp_dir = pathlib.Path(tempfile.mkdtemp())
343+
tmp_file = tmp_dir / "pwn"
344+
unsafe_options = [
345+
f"--upload-pack='touch {tmp_file}'",
346+
f"-u 'touch {tmp_file}'",
347+
]
348+
for i, unsafe_option in enumerate(unsafe_options):
349+
destination = tmp_dir / str(i)
350+
assert not tmp_file.exists()
351+
# The options will be allowed, but the command will fail.
352+
with self.assertRaises(GitCommandError):
353+
Repo.clone_from(
354+
rw_repo.working_dir, destination, multi_options=[unsafe_option], allow_unsafe_options=True
355+
)
356+
assert tmp_file.exists()
357+
tmp_file.unlink()
358+
359+
unsafe_options = [
360+
"--config=protocol.ext.allow=always",
361+
"-c protocol.ext.allow=always",
362+
]
363+
for i, unsafe_option in enumerate(unsafe_options):
364+
destination = tmp_dir / str(i)
365+
assert not destination.exists()
366+
Repo.clone_from(rw_repo.working_dir, destination, multi_options=[unsafe_option], allow_unsafe_options=True)
367+
assert destination.exists()
368+
369+
@with_rw_repo("HEAD")
370+
def test_clone_from_safe_options(self, rw_repo):
371+
tmp_dir = pathlib.Path(tempfile.mkdtemp())
372+
options = [
373+
"--depth=1",
374+
"--single-branch",
375+
"-q",
376+
]
377+
for option in options:
378+
destination = tmp_dir / option
379+
assert not destination.exists()
380+
Repo.clone_from(rw_repo.common_dir, destination, multi_options=[option])
381+
assert destination.exists()
382+
383+
def test_clone_from_unsafe_procol(self):
384+
tmp_dir = pathlib.Path(tempfile.mkdtemp())
385+
tmp_file = tmp_dir / "pwn"
386+
urls = [
387+
f"ext::sh -c touch% {tmp_file}",
388+
"fd::17/foo",
389+
]
390+
for url in urls:
391+
with self.assertRaises(UnsafeProtocolError):
392+
Repo.clone_from(url, tmp_dir)
393+
assert not tmp_file.exists()
394+
395+
def test_clone_from_unsafe_procol_allowed(self):
396+
tmp_dir = pathlib.Path(tempfile.mkdtemp())
397+
tmp_file = tmp_dir / "pwn"
398+
urls = [
399+
"ext::sh -c touch% /tmp/pwn",
400+
"fd::/foo",
401+
]
402+
for url in urls:
403+
# The URL will be allowed into the command, but the command will
404+
# fail since we don't have that protocol enabled in the Git config file.
405+
with self.assertRaises(GitCommandError):
406+
Repo.clone_from(url, tmp_dir, allow_unsafe_protocols=True)
407+
assert not tmp_file.exists()
408+
266409
@with_rw_repo("HEAD")
267410
def test_max_chunk_size(self, repo):
268411
class TestOutputStream(TestBase):

‎test/test_submodule.py

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
44
import os
55
import shutil
6+
import tempfile
7+
from pathlib import Path
68
import sys
79
from unittest import skipIf
810

@@ -12,7 +14,13 @@
1214
from git.cmd import Git
1315
from git.compat import is_win
1416
from git.config import GitConfigParser, cp
15-
from git.exc import InvalidGitRepositoryError, RepositoryDirtyError
17+
from git.exc import (
18+
GitCommandError,
19+
InvalidGitRepositoryError,
20+
RepositoryDirtyError,
21+
UnsafeOptionError,
22+
UnsafeProtocolError,
23+
)
1624
from git.objects.submodule.base import Submodule
1725
from git.objects.submodule.root import RootModule, RootUpdateProgress
1826
from git.repo.fun import find_submodule_git_dir, touch
@@ -1026,7 +1034,7 @@ def test_update_clone_multi_options_argument(self, rwdir):
10261034
)
10271035

10281036
# Act
1029-
sm.update(init=True, clone_multi_options=["--config core.eol=true"])
1037+
sm.update(init=True, clone_multi_options=["--config core.eol=true"], allow_unsafe_options=True)
10301038

10311039
# Assert
10321040
sm_config = GitConfigParser(file_or_files=osp.join(parent.git_dir, "modules", sm_name, "config"))
@@ -1070,6 +1078,7 @@ def test_add_clone_multi_options_argument(self, rwdir):
10701078
sm_name,
10711079
url=self._small_repo_url(),
10721080
clone_multi_options=["--config core.eol=true"],
1081+
allow_unsafe_options=True,
10731082
)
10741083

10751084
# Assert
@@ -1089,3 +1098,142 @@ def test_add_no_clone_multi_options_argument(self, rwdir):
10891098
sm_config = GitConfigParser(file_or_files=osp.join(parent.git_dir, "modules", sm_name, "config"))
10901099
with self.assertRaises(cp.NoOptionError):
10911100
sm_config.get_value("core", "eol")
1101+
1102+
@with_rw_repo("HEAD")
1103+
def test_submodule_add_unsafe_url(self, rw_repo):
1104+
tmp_dir = Path(tempfile.mkdtemp())
1105+
tmp_file = tmp_dir / "pwn"
1106+
urls = [
1107+
f"ext::sh -c touch% {tmp_file}",
1108+
"fd::/foo",
1109+
]
1110+
for url in urls:
1111+
with self.assertRaises(UnsafeProtocolError):
1112+
Submodule.add(rw_repo, "new", "new", url)
1113+
assert not tmp_file.exists()
1114+
1115+
@with_rw_repo("HEAD")
1116+
def test_submodule_add_unsafe_url_allowed(self, rw_repo):
1117+
tmp_dir = Path(tempfile.mkdtemp())
1118+
tmp_file = tmp_dir / "pwn"
1119+
urls = [
1120+
f"ext::sh -c touch% {tmp_file}",
1121+
"fd::/foo",
1122+
]
1123+
for url in urls:
1124+
# The URL will be allowed into the command, but the command will
1125+
# fail since we don't have that protocol enabled in the Git config file.
1126+
with self.assertRaises(GitCommandError):
1127+
Submodule.add(rw_repo, "new", "new", url, allow_unsafe_protocols=True)
1128+
assert not tmp_file.exists()
1129+
1130+
@with_rw_repo("HEAD")
1131+
def test_submodule_add_unsafe_options(self, rw_repo):
1132+
tmp_dir = Path(tempfile.mkdtemp())
1133+
tmp_file = tmp_dir / "pwn"
1134+
unsafe_options = [
1135+
f"--upload-pack='touch {tmp_file}'",
1136+
f"-u 'touch {tmp_file}'",
1137+
"--config=protocol.ext.allow=always",
1138+
"-c protocol.ext.allow=always",
1139+
]
1140+
for unsafe_option in unsafe_options:
1141+
with self.assertRaises(UnsafeOptionError):
1142+
Submodule.add(rw_repo, "new", "new", str(tmp_dir), clone_multi_options=[unsafe_option])
1143+
assert not tmp_file.exists()
1144+
1145+
@with_rw_repo("HEAD")
1146+
def test_submodule_add_unsafe_options_allowed(self, rw_repo):
1147+
tmp_dir = Path(tempfile.mkdtemp())
1148+
tmp_file = tmp_dir / "pwn"
1149+
unsafe_options = [
1150+
f"--upload-pack='touch {tmp_file}'",
1151+
f"-u 'touch {tmp_file}'",
1152+
]
1153+
for unsafe_option in unsafe_options:
1154+
# The options will be allowed, but the command will fail.
1155+
with self.assertRaises(GitCommandError):
1156+
Submodule.add(
1157+
rw_repo, "new", "new", str(tmp_dir), clone_multi_options=[unsafe_option], allow_unsafe_options=True
1158+
)
1159+
assert not tmp_file.exists()
1160+
1161+
unsafe_options = [
1162+
"--config=protocol.ext.allow=always",
1163+
"-c protocol.ext.allow=always",
1164+
]
1165+
for unsafe_option in unsafe_options:
1166+
with self.assertRaises(GitCommandError):
1167+
Submodule.add(
1168+
rw_repo, "new", "new", str(tmp_dir), clone_multi_options=[unsafe_option], allow_unsafe_options=True
1169+
)
1170+
1171+
@with_rw_repo("HEAD")
1172+
def test_submodule_update_unsafe_url(self, rw_repo):
1173+
tmp_dir = Path(tempfile.mkdtemp())
1174+
tmp_file = tmp_dir / "pwn"
1175+
urls = [
1176+
f"ext::sh -c touch% {tmp_file}",
1177+
"fd::/foo",
1178+
]
1179+
for url in urls:
1180+
submodule = Submodule(rw_repo, b"\0" * 20, name="new", path="new", url=url)
1181+
with self.assertRaises(UnsafeProtocolError):
1182+
submodule.update()
1183+
assert not tmp_file.exists()
1184+
1185+
@with_rw_repo("HEAD")
1186+
def test_submodule_update_unsafe_url_allowed(self, rw_repo):
1187+
tmp_dir = Path(tempfile.mkdtemp())
1188+
tmp_file = tmp_dir / "pwn"
1189+
urls = [
1190+
f"ext::sh -c touch% {tmp_file}",
1191+
"fd::/foo",
1192+
]
1193+
for url in urls:
1194+
submodule = Submodule(rw_repo, b"\0" * 20, name="new", path="new", url=url)
1195+
# The URL will be allowed into the command, but the command will
1196+
# fail since we don't have that protocol enabled in the Git config file.
1197+
with self.assertRaises(GitCommandError):
1198+
submodule.update(allow_unsafe_protocols=True)
1199+
assert not tmp_file.exists()
1200+
1201+
@with_rw_repo("HEAD")
1202+
def test_submodule_update_unsafe_options(self, rw_repo):
1203+
tmp_dir = Path(tempfile.mkdtemp())
1204+
tmp_file = tmp_dir / "pwn"
1205+
unsafe_options = [
1206+
f"--upload-pack='touch {tmp_file}'",
1207+
f"-u 'touch {tmp_file}'",
1208+
"--config=protocol.ext.allow=always",
1209+
"-c protocol.ext.allow=always",
1210+
]
1211+
submodule = Submodule(rw_repo, b"\0" * 20, name="new", path="new", url=str(tmp_dir))
1212+
for unsafe_option in unsafe_options:
1213+
with self.assertRaises(UnsafeOptionError):
1214+
submodule.update(clone_multi_options=[unsafe_option])
1215+
assert not tmp_file.exists()
1216+
1217+
@with_rw_repo("HEAD")
1218+
def test_submodule_update_unsafe_options_allowed(self, rw_repo):
1219+
tmp_dir = Path(tempfile.mkdtemp())
1220+
tmp_file = tmp_dir / "pwn"
1221+
unsafe_options = [
1222+
f"--upload-pack='touch {tmp_file}'",
1223+
f"-u 'touch {tmp_file}'",
1224+
]
1225+
submodule = Submodule(rw_repo, b"\0" * 20, name="new", path="new", url=str(tmp_dir))
1226+
for unsafe_option in unsafe_options:
1227+
# The options will be allowed, but the command will fail.
1228+
with self.assertRaises(GitCommandError):
1229+
submodule.update(clone_multi_options=[unsafe_option], allow_unsafe_options=True)
1230+
assert not tmp_file.exists()
1231+
1232+
unsafe_options = [
1233+
"--config=protocol.ext.allow=always",
1234+
"-c protocol.ext.allow=always",
1235+
]
1236+
submodule = Submodule(rw_repo, b"\0" * 20, name="new", path="new", url=str(tmp_dir))
1237+
for unsafe_option in unsafe_options:
1238+
with self.assertRaises(GitCommandError):
1239+
submodule.update(clone_multi_options=[unsafe_option], allow_unsafe_options=True)

0 commit comments

Comments
 (0)
Please sign in to comment.