VYPR
Critical severityNVD Advisory· Published Dec 12, 2022· Updated Nov 3, 2025

Remote Code Execution (RCE)

CVE-2022-24439

Description

All versions of package gitpython are vulnerable to Remote Code Execution (RCE) due to improper user input validation, which makes it possible to inject a maliciously crafted remote URL into the clone command. Exploiting this vulnerability is possible because the library makes external calls to git without sufficient sanitization of input arguments.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

GitPython is vulnerable to remote code execution via unsanitized remote URLs during clone operations.

Root

Cause

CVE-2022-24439 is a critical vulnerability in the GitPython library that stems from insufficient validation of user-supplied remote URLs passed to the Repo.clone() and Repo.clone_from() methods. The library invokes the external git clone command without properly sanitizing the URL argument, allowing an attacker to inject arbitrary command-line arguments or use unsafe protocol schemes such as ext::. This enables the execution of arbitrary commands on the host system [1][4].

Exploitation

An attacker can exploit this flaw by providing a crafted remote URL to an application that uses GitPython's clone functionality. The malicious URL, when passed unmodified to git clone, can include protocol options like --config or use the ext:: remote helper to execute shell commands. The vulnerability requires no authentication beyond the user context of the calling process, and the attacker only needs to control the URL input (e.g., through user-provided repository links) [1][3].

Impact

Successful exploitation results in Remote Code Execution (RCE) with the privileges of the user running the GitPython-based application. An attacker can execute arbitrary OS commands, potentially leading to full system compromise, data theft, or lateral movement within a network. The vulnerability is classified with a CVSS score of 9.8 (Critical) due to its low attack complexity and no required privileges [1].

Mitigation

The vulnerability was patched in GitPython release 3.1.30 by introducing the unsafe_protocols parameter and blocking dangerous protocol options by default. The fix adds validation to reject ext:: URLs and options like --upload-pack or --config protocol.* unless explicitly opted in by the application [3]. Users should upgrade to GitPython ≥3.1.30 immediately. As of the advisory, no workarounds are available for earlier versions [2][4]. The project is in maintenance mode, so further security patches depend on community contributions [2].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
GitPythonPyPI
< 3.1.303.1.30

Affected products

2

Patches

1
2625ed9fc074

Forbid unsafe protocol URLs in Repo.clone{,_from}()

https://github.com/gitpython-developers/GitPythonSteve KowalikDec 20, 2022via ghsa
3 files changed · +70 1
  • git/exc.py+4 0 modified
    @@ -37,6 +37,10 @@ class NoSuchPathError(GitError, OSError):
         """Thrown if a path could not be access by the system."""
     
     
    +class UnsafeOptionsUsedError(GitError):
    +    """Thrown if unsafe protocols or options are passed without overridding."""
    +
    +
     class CommandError(GitError):
         """Base class for exceptions thrown at every stage of `Popen()` execution.
     
    
  • git/repo/base.py+30 1 modified
    @@ -21,7 +21,12 @@
     )
     from git.config import GitConfigParser
     from git.db import GitCmdObjectDB
    -from git.exc import InvalidGitRepositoryError, NoSuchPathError, GitCommandError
    +from git.exc import (
    +    GitCommandError,
    +    InvalidGitRepositoryError,
    +    NoSuchPathError,
    +    UnsafeOptionsUsedError,
    +)
     from git.index import IndexFile
     from git.objects import Submodule, RootModule, Commit
     from git.refs import HEAD, Head, Reference, TagReference
    @@ -128,6 +133,7 @@ class Repo(object):
         re_envvars = re.compile(r"(\$(\{\s?)?[a-zA-Z_]\w*(\}\s?)?|%\s?[a-zA-Z_]\w*\s?%)")
         re_author_committer_start = re.compile(r"^(author|committer)")
         re_tab_full_line = re.compile(r"^\t(.*)$")
    +    re_config_protocol_option = re.compile(r"-[-]?c(|onfig)\s+protocol\.", re.I)
     
         # invariants
         # represents the configuration level of a configuration file
    @@ -1215,11 +1221,27 @@ def _clone(
             # END handle remote repo
             return repo
     
    +    @classmethod
    +    def unsafe_options(
    +        cls,
    +        url: str,
    +        multi_options: Optional[List[str]] = None,
    +    ) -> bool:
    +        if "ext::" in url:
    +            return True
    +        if multi_options is not None:
    +            if any(["--upload-pack" in m for m in multi_options]):
    +                return True
    +            if any([re.match(cls.re_config_protocol_option, m) for m in multi_options]):
    +                return True
    +        return False
    +
         def clone(
             self,
             path: PathLike,
             progress: Optional[Callable] = None,
             multi_options: Optional[List[str]] = None,
    +        unsafe_protocols: bool = False,
             **kwargs: Any,
         ) -> "Repo":
             """Create a clone from this repository.
    @@ -1230,12 +1252,15 @@ def clone(
                 option per list item which is passed exactly as specified to clone.
                 For example ['--config core.filemode=false', '--config core.ignorecase',
                 '--recurse-submodule=repo1_path', '--recurse-submodule=repo2_path']
    +        :param unsafe_protocols: Allow unsafe protocols to be used, like ext
             :param kwargs:
                 * odbt = ObjectDatabase Type, allowing to determine the object database
                   implementation used by the returned Repo instance
                 * All remaining keyword arguments are given to the git-clone command
     
             :return: ``git.Repo`` (the newly cloned repo)"""
    +        if not unsafe_protocols and self.unsafe_options(path, multi_options):
    +            raise UnsafeOptionsUsedError(f"{path} requires unsafe_protocols flag")
             return self._clone(
                 self.git,
                 self.common_dir,
    @@ -1254,6 +1279,7 @@ def clone_from(
             progress: Optional[Callable] = None,
             env: Optional[Mapping[str, str]] = None,
             multi_options: Optional[List[str]] = None,
    +        unsafe_protocols: bool = False,
             **kwargs: Any,
         ) -> "Repo":
             """Create a clone from the given URL
    @@ -1268,11 +1294,14 @@ def clone_from(
                 If you want to unset some variable, consider providing empty string
                 as its value.
             :param multi_options: See ``clone`` method
    +        :param unsafe_protocols: Allow unsafe protocols to be used, like ext
             :param kwargs: see the ``clone`` method
             :return: Repo instance pointing to the cloned directory"""
             git = cls.GitCommandWrapperType(os.getcwd())
             if env is not None:
                 git.update_environment(**env)
    +        if not unsafe_protocols and cls.unsafe_options(url, multi_options):
    +            raise UnsafeOptionsUsedError(f"{url} requires unsafe_protocols flag")
             return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs)
     
         def archive(
    
  • test/test_repo.py+36 0 modified
    @@ -13,6 +13,7 @@
     import pickle
     import sys
     import tempfile
    +import uuid
     from unittest import mock, skipIf, SkipTest
     
     import pytest
    @@ -37,6 +38,7 @@
     )
     from git.exc import (
         BadObject,
    +    UnsafeOptionsUsedError,
     )
     from git.repo.fun import touch
     from test.lib import TestBase, with_rw_repo, fixture
    @@ -263,6 +265,40 @@ def test_leaking_password_in_clone_logs(self, rw_dir):
                 to_path=rw_dir,
             )
     
    +    def test_unsafe_options(self):
    +        self.assertFalse(Repo.unsafe_options("github.com/deploy/deploy"))
    +
    +    def test_unsafe_options_ext_url(self):
    +        self.assertTrue(Repo.unsafe_options("ext::ssh"))
    +
    +    def test_unsafe_options_multi_options_upload_pack(self):
    +        self.assertTrue(Repo.unsafe_options("", ["--upload-pack='touch foo'"]))
    +
    +    def test_unsafe_options_multi_options_config_user(self):
    +        self.assertFalse(Repo.unsafe_options("", ["--config user"]))
    +
    +    def test_unsafe_options_multi_options_config_protocol(self):
    +        self.assertTrue(Repo.unsafe_options("", ["--config protocol.foo"]))
    +
    +    def test_clone_from_forbids_helper_urls_by_default(self):
    +        with self.assertRaises(UnsafeOptionsUsedError):
    +            Repo.clone_from("ext::sh -c touch% /tmp/foo", "tmp")
    +
    +    @with_rw_repo("HEAD")
    +    def test_clone_from_allow_unsafe(self, repo):
    +        bad_filename = pathlib.Path(f'{tempfile.gettempdir()}/{uuid.uuid4()}')
    +        bad_url = f'ext::sh -c touch% {bad_filename}'
    +        try:
    +            repo.clone_from(
    +                bad_url, 'tmp',
    +                multi_options=["-c protocol.ext.allow=always"],
    +                unsafe_protocols=True
    +            )
    +        except GitCommandError:
    +            pass
    +        self.assertTrue(bad_filename.is_file())
    +        bad_filename.unlink()
    +
         @with_rw_repo("HEAD")
         def test_max_chunk_size(self, repo):
             class TestOutputStream(TestBase):
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

24

News mentions

0

No linked articles in our index yet.