Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 2625ed9

Browse filesBrowse files
s-t-e-v-e-n-kstsewd
authored andcommitted
Forbid unsafe protocol URLs in Repo.clone{,_from}()
Since the URL is passed directly to git clone, and the remote-ext helper will happily execute shell commands, so by default disallow URLs that contain a "::" unless a new unsafe_protocols kwarg is passed. (CVE-2022-24439) Fixes #1515
1 parent 787359d commit 2625ed9
Copy full SHA for 2625ed9

3 files changed

+70-1Lines changed: 70 additions & 1 deletion

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎git/exc.py‎

Copy file name to clipboardExpand all lines: git/exc.py
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ class NoSuchPathError(GitError, OSError):
3737
"""Thrown if a path could not be access by the system."""
3838

3939

40+
class UnsafeOptionsUsedError(GitError):
41+
"""Thrown if unsafe protocols or options are passed without overridding."""
42+
43+
4044
class CommandError(GitError):
4145
"""Base class for exceptions thrown at every stage of `Popen()` execution.
4246
Collapse file

‎git/repo/base.py‎

Copy file name to clipboardExpand all lines: git/repo/base.py
+30-1Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@
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+
UnsafeOptionsUsedError,
29+
)
2530
from git.index import IndexFile
2631
from git.objects import Submodule, RootModule, Commit
2732
from git.refs import HEAD, Head, Reference, TagReference
@@ -128,6 +133,7 @@ class Repo(object):
128133
re_envvars = re.compile(r"(\$(\{\s?)?[a-zA-Z_]\w*(\}\s?)?|%\s?[a-zA-Z_]\w*\s?%)")
129134
re_author_committer_start = re.compile(r"^(author|committer)")
130135
re_tab_full_line = re.compile(r"^\t(.*)$")
136+
re_config_protocol_option = re.compile(r"-[-]?c(|onfig)\s+protocol\.", re.I)
131137

132138
# invariants
133139
# represents the configuration level of a configuration file
@@ -1215,11 +1221,27 @@ def _clone(
12151221
# END handle remote repo
12161222
return repo
12171223

1224+
@classmethod
1225+
def unsafe_options(
1226+
cls,
1227+
url: str,
1228+
multi_options: Optional[List[str]] = None,
1229+
) -> bool:
1230+
if "ext::" in url:
1231+
return True
1232+
if multi_options is not None:
1233+
if any(["--upload-pack" in m for m in multi_options]):
1234+
return True
1235+
if any([re.match(cls.re_config_protocol_option, m) for m in multi_options]):
1236+
return True
1237+
return False
1238+
12181239
def clone(
12191240
self,
12201241
path: PathLike,
12211242
progress: Optional[Callable] = None,
12221243
multi_options: Optional[List[str]] = None,
1244+
unsafe_protocols: bool = False,
12231245
**kwargs: Any,
12241246
) -> "Repo":
12251247
"""Create a clone from this repository.
@@ -1230,12 +1252,15 @@ def clone(
12301252
option per list item which is passed exactly as specified to clone.
12311253
For example ['--config core.filemode=false', '--config core.ignorecase',
12321254
'--recurse-submodule=repo1_path', '--recurse-submodule=repo2_path']
1255+
:param unsafe_protocols: Allow unsafe protocols to be used, like ext
12331256
:param kwargs:
12341257
* odbt = ObjectDatabase Type, allowing to determine the object database
12351258
implementation used by the returned Repo instance
12361259
* All remaining keyword arguments are given to the git-clone command
12371260
12381261
:return: ``git.Repo`` (the newly cloned repo)"""
1262+
if not unsafe_protocols and self.unsafe_options(path, multi_options):
1263+
raise UnsafeOptionsUsedError(f"{path} requires unsafe_protocols flag")
12391264
return self._clone(
12401265
self.git,
12411266
self.common_dir,
@@ -1254,6 +1279,7 @@ def clone_from(
12541279
progress: Optional[Callable] = None,
12551280
env: Optional[Mapping[str, str]] = None,
12561281
multi_options: Optional[List[str]] = None,
1282+
unsafe_protocols: bool = False,
12571283
**kwargs: Any,
12581284
) -> "Repo":
12591285
"""Create a clone from the given URL
@@ -1268,11 +1294,14 @@ def clone_from(
12681294
If you want to unset some variable, consider providing empty string
12691295
as its value.
12701296
:param multi_options: See ``clone`` method
1297+
:param unsafe_protocols: Allow unsafe protocols to be used, like ext
12711298
:param kwargs: see the ``clone`` method
12721299
:return: Repo instance pointing to the cloned directory"""
12731300
git = cls.GitCommandWrapperType(os.getcwd())
12741301
if env is not None:
12751302
git.update_environment(**env)
1303+
if not unsafe_protocols and cls.unsafe_options(url, multi_options):
1304+
raise UnsafeOptionsUsedError(f"{url} requires unsafe_protocols flag")
12761305
return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs)
12771306

12781307
def archive(
Collapse file

‎test/test_repo.py‎

Copy file name to clipboardExpand all lines: test/test_repo.py
+36Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import pickle
1414
import sys
1515
import tempfile
16+
import uuid
1617
from unittest import mock, skipIf, SkipTest
1718

1819
import pytest
@@ -37,6 +38,7 @@
3738
)
3839
from git.exc import (
3940
BadObject,
41+
UnsafeOptionsUsedError,
4042
)
4143
from git.repo.fun import touch
4244
from test.lib import TestBase, with_rw_repo, fixture
@@ -263,6 +265,40 @@ def test_leaking_password_in_clone_logs(self, rw_dir):
263265
to_path=rw_dir,
264266
)
265267

268+
def test_unsafe_options(self):
269+
self.assertFalse(Repo.unsafe_options("github.com/deploy/deploy"))
270+
271+
def test_unsafe_options_ext_url(self):
272+
self.assertTrue(Repo.unsafe_options("ext::ssh"))
273+
274+
def test_unsafe_options_multi_options_upload_pack(self):
275+
self.assertTrue(Repo.unsafe_options("", ["--upload-pack='touch foo'"]))
276+
277+
def test_unsafe_options_multi_options_config_user(self):
278+
self.assertFalse(Repo.unsafe_options("", ["--config user"]))
279+
280+
def test_unsafe_options_multi_options_config_protocol(self):
281+
self.assertTrue(Repo.unsafe_options("", ["--config protocol.foo"]))
282+
283+
def test_clone_from_forbids_helper_urls_by_default(self):
284+
with self.assertRaises(UnsafeOptionsUsedError):
285+
Repo.clone_from("ext::sh -c touch% /tmp/foo", "tmp")
286+
287+
@with_rw_repo("HEAD")
288+
def test_clone_from_allow_unsafe(self, repo):
289+
bad_filename = pathlib.Path(f'{tempfile.gettempdir()}/{uuid.uuid4()}')
290+
bad_url = f'ext::sh -c touch% {bad_filename}'
291+
try:
292+
repo.clone_from(
293+
bad_url, 'tmp',
294+
multi_options=["-c protocol.ext.allow=always"],
295+
unsafe_protocols=True
296+
)
297+
except GitCommandError:
298+
pass
299+
self.assertTrue(bad_filename.is_file())
300+
bad_filename.unlink()
301+
266302
@with_rw_repo("HEAD")
267303
def test_max_chunk_size(self, repo):
268304
class TestOutputStream(TestBase):

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.