Skip to content

Commit 428d205

Browse files
codexByron
authored andcommitted
fix(repo): validate rendered archive remote options
<!-- Byron --> rubber-stamp, after checking diff quickly. <!-- agent --> `Repo.archive()` checked only the original `remote` keyword. Git accepts abbreviated long options, while GitPython renders sequence values as repeated options and permits a value embedded in the keyword name. Those spellings could bypass unsafe-protocol validation (finding 21492's broader URL-validation class). Inspect the arguments produced by the existing `Git.transform_kwargs()` and validate every emitted `--remote` value, including its abbreviations, with the shared protocol checker. Preserve safe repeated IPv6 URLs and keep unsafe-option and unsafe-protocol opt-ins independent.
1 parent 9e8c85e commit 428d205

2 files changed

Lines changed: 65 additions & 11 deletions

File tree

‎git/repo/base.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1778,9 +1778,13 @@ def archive(
17781778
treeish = self.head.commit
17791779
if prefix and "prefix" not in kwargs:
17801780
kwargs["prefix"] = prefix
1781-
remote = kwargs.get("remote")
1782-
if not allow_unsafe_protocols and remote is not None:
1783-
Git.check_unsafe_protocols(str(remote))
1781+
if not allow_unsafe_protocols:
1782+
# Check the emitted URL, including repeated values and Git's long-option
1783+
# abbreviations, rather than only the untransformed `remote` keyword.
1784+
for arg in self.git.transform_kwargs(**kwargs):
1785+
option, separator, remote = arg.partition("=")
1786+
if separator and option.startswith("--r") and "--remote".startswith(option):
1787+
Git.check_unsafe_protocols(remote)
17841788
if not allow_unsafe_options:
17851789
Git.check_unsafe_options(
17861790
options=Git._option_candidates([], kwargs),

‎test/test_repo.py‎

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66
import gc
77
import glob
88
import io
9-
from io import BytesIO
109
import itertools
1110
import os
1211
import os.path as osp
1312
import pathlib
1413
import pickle
1514
import sys
1615
import tempfile
17-
from unittest import mock
16+
from io import BytesIO
1817
from pathlib import Path
18+
from unittest import mock
1919

2020
import pytest
2121

@@ -37,14 +37,10 @@
3737
Submodule,
3838
Tree,
3939
)
40-
from git.exc import UnsafeOptionError
41-
from git.exc import UnsafeProtocolError
42-
from git.exc import BadObject
43-
from git.exc import WorkTreeRepositoryUnsupported
40+
from git.exc import BadObject, UnsafeOptionError, UnsafeProtocolError, WorkTreeRepositoryUnsupported
4441
from git.repo.fun import find_worktree_git_dir, touch
4542
from git.util import bin_to_hex, cwd, cygpath, join_path_native, rmfile, rmtree
46-
47-
from test.lib import TestBase, fixture, requires_symlinks, with_rw_directory, with_rw_repo, PathLikeMock
43+
from test.lib import PathLikeMock, TestBase, fixture, requires_symlinks, with_rw_directory, with_rw_repo
4844

4945

5046
def iter_flatten(lol):
@@ -1672,3 +1668,57 @@ def test_ignored_raises_error_w_symlink(self):
16721668

16731669
with pytest.raises(GitCommandError):
16741670
temp_repo.ignored(tmp_dir / "symlink/file.txt")
1671+
1672+
1673+
@pytest.mark.parametrize("allow_unsafe_options", (False, True))
1674+
@pytest.mark.parametrize(
1675+
"kwargs",
1676+
(
1677+
{"rem": "ext::helper"},
1678+
{"re": "ext::helper"},
1679+
{"remo": "ext::helper"},
1680+
{"remot": "ext::helper"}, # codespell:ignore remot
1681+
{"remote": ["https://example.com/repo", "ext::helper"]},
1682+
{"remote": (None, False, "ext::helper")},
1683+
{"remote=ext::helper": True},
1684+
{"r=ext::helper": True},
1685+
{"remote": "ext://helper"},
1686+
),
1687+
)
1688+
def test_archive_protocol_guard_checks_emitted_remote_options(tmp_path, kwargs, allow_unsafe_options):
1689+
with Repo.init(tmp_path) as repo, mock.patch.object(Git, "execute") as execute:
1690+
with pytest.raises(UnsafeProtocolError):
1691+
repo.archive(BytesIO(), "HEAD", allow_unsafe_options=allow_unsafe_options, **kwargs)
1692+
execute.assert_not_called()
1693+
1694+
1695+
def test_archive_preserves_safe_repeated_remote_options(tmp_path):
1696+
urls = ["https://[::1]/repo.git", "ssh://git@[2001:db8::1]/repo.git"]
1697+
with Repo.init(tmp_path) as repo, mock.patch.object(Git, "execute") as execute:
1698+
output = BytesIO()
1699+
repo.archive(output, "HEAD", rem=urls)
1700+
execute.assert_called_once_with(
1701+
[Git.GIT_PYTHON_GIT_EXECUTABLE, "archive", *(f"--rem={url}" for url in urls), "--", "HEAD"],
1702+
output_stream=output,
1703+
)
1704+
1705+
1706+
def test_archive_protocol_and_option_opt_ins_are_independent(tmp_path):
1707+
with Repo.init(tmp_path) as repo, mock.patch.object(Git, "execute") as execute:
1708+
with pytest.raises(UnsafeOptionError):
1709+
repo.archive(BytesIO(), "HEAD", rem=["ext::helper"], exec="helper", allow_unsafe_protocols=True)
1710+
execute.assert_not_called()
1711+
1712+
output = BytesIO()
1713+
repo.archive(
1714+
output,
1715+
"HEAD",
1716+
rem=["ext::helper"],
1717+
exec="helper",
1718+
allow_unsafe_options=True,
1719+
allow_unsafe_protocols=True,
1720+
)
1721+
execute.assert_called_once_with(
1722+
[Git.GIT_PYTHON_GIT_EXECUTABLE, "archive", "--rem=ext::helper", "--exec=helper", "--", "HEAD"],
1723+
output_stream=output,
1724+
)

0 commit comments

Comments
 (0)