Skip to content

Commit 1d51b89

Browse files
codexByron
andcommitted
fix: guard diff output options
<!-- agent --> Reject unsafe diff options before revision parsing or Git invocation so callers cannot write command output to arbitrary filesystem paths. Cover commit and index diffs, including option-like revisions, and preserve an explicit allow_unsafe_options escape hatch. References GHSA-fjr4-x663-mwxc. Validated against Git baseline a23bace9. Co-authored-by: Sebastian Thiel <sebastian.thiel@icloud.com>
1 parent ffcb535 commit 1d51b89

3 files changed

Lines changed: 48 additions & 4 deletions

File tree

git/diff.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import re
1010
import warnings
1111

12-
from git.cmd import handle_process_output
12+
from git.cmd import Git, handle_process_output
1313
from git.compat import defenc
1414
from git.objects.blob import Blob
1515
from git.objects.util import mode_str_to_int
@@ -35,7 +35,6 @@
3535
if TYPE_CHECKING:
3636
from subprocess import Popen
3737

38-
from git.cmd import Git
3938
from git.objects.base import IndexObject
4039
from git.objects.commit import Commit
4140
from git.objects.tree import Tree
@@ -190,6 +189,7 @@ def diff(
190189
other: Union[DiffConstants, "Tree", "Commit", str, None] = INDEX,
191190
paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
192191
create_patch: bool = False,
192+
allow_unsafe_options: bool = False,
193193
**kwargs: Any,
194194
) -> "DiffIndex[Diff]":
195195
"""Create diffs between two items being trees, trees and index or an index and
@@ -219,6 +219,10 @@ def diff(
219219
applied makes the self to other. Patches are somewhat costly as blobs have
220220
to be read and diffed.
221221
222+
:param allow_unsafe_options:
223+
If ``True``, allow options such as ``--output`` that can write to arbitrary
224+
filesystem paths.
225+
222226
:param kwargs:
223227
Additional arguments passed to :manpage:`git-diff(1)`, such as ``R=True`` to
224228
swap both sides of the diff.
@@ -231,6 +235,12 @@ def diff(
231235
an instance of :class:`~git.objects.tree.Tree` or
232236
:class:`~git.objects.commit.Commit`, or a git command error will occur.
233237
"""
238+
if not allow_unsafe_options:
239+
Git.check_unsafe_options(
240+
options=Git._option_candidates([other], kwargs),
241+
unsafe_options=self.repo.unsafe_git_revision_options,
242+
)
243+
234244
args: List[Union[PathLike, Diffable]] = []
235245
args.append("--abbrev=40") # We need full shas.
236246
args.append("--full-index") # Get full index paths, not only filenames.

git/index/base.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from gitdb.db import MemoryDB
2424

2525
from git.compat import defenc, force_bytes
26+
from git.cmd import Git
2627
import git.diff as git_diff
2728
from git.exc import CheckoutError, GitCommandError, GitError, InvalidGitRepositoryError
2829
from git.objects import Blob, Commit, Object, Submodule, Tree
@@ -1492,6 +1493,7 @@ def diff(
14921493
] = git_diff.INDEX,
14931494
paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
14941495
create_patch: bool = False,
1496+
allow_unsafe_options: bool = False,
14951497
**kwargs: Any,
14961498
) -> git_diff.DiffIndex[git_diff.Diff]:
14971499
"""Diff this index against the working copy or a :class:`~git.objects.tree.Tree`
@@ -1504,6 +1506,12 @@ def diff(
15041506
Will only work with indices that represent the default git index as they
15051507
have not been initialized with a stream.
15061508
"""
1509+
if not allow_unsafe_options:
1510+
Git.check_unsafe_options(
1511+
options=Git._option_candidates([other], kwargs),
1512+
unsafe_options=self.repo.unsafe_git_revision_options,
1513+
)
1514+
15071515
# Only run if we are the default repository index.
15081516
if self._file_path != self._index_path():
15091517
raise AssertionError("Cannot call %r on indices that do not represent the default git index" % self.diff())
@@ -1560,12 +1568,18 @@ def diff(
15601568
# Invert the existing R flag.
15611569
cur_val = kwargs.get("R", False)
15621570
kwargs["R"] = not cur_val
1563-
return other.diff(self.INDEX, paths, create_patch, **kwargs)
1571+
return other.diff(
1572+
self.INDEX,
1573+
paths,
1574+
create_patch,
1575+
allow_unsafe_options=allow_unsafe_options,
1576+
**kwargs,
1577+
)
15641578
# END diff against other item handling
15651579

15661580
# If other is not None here, something is wrong.
15671581
if other is not None:
15681582
raise ValueError("other must be None, Diffable.INDEX, a Tree or Commit, was %r" % other)
15691583

15701584
# Diff against working copy - can be handled by superclass natively.
1571-
return super().diff(other, paths, create_patch, **kwargs)
1585+
return super().diff(other, paths, create_patch, allow_unsafe_options=allow_unsafe_options, **kwargs)

test/test_diff.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from git import NULL_TREE, Diff, DiffIndex, Diffable, GitCommandError, Repo, Submodule
1616
from git.cmd import Git
17+
from git.exc import UnsafeOptionError
1718

1819
from test.lib import StringProcessAdapter, TestBase, fixture, with_rw_directory
1920

@@ -352,6 +353,25 @@ def test_diff_submodule(self):
352353
self.assertIsInstance(diff.a_blob.size, int)
353354
self.assertIsInstance(diff.b_blob.size, int)
354355

356+
def test_diff_rejects_unsafe_output_options(self):
357+
commit = self.rorepo.head.commit
358+
359+
calls = (
360+
lambda target: commit.diff(output=target),
361+
lambda target: commit.diff(other=f"--output={target}"),
362+
lambda target: self.rorepo.index.diff(NULL_TREE, output=target),
363+
lambda target: self.rorepo.index.diff(f"--output={target}"),
364+
)
365+
for index, call in enumerate(calls):
366+
target = osp.join(self.repo_dir, f"diff-output-{index}")
367+
with self.assertRaises(UnsafeOptionError):
368+
call(target)
369+
self.assertFalse(osp.exists(target))
370+
371+
allowed_target = osp.join(self.repo_dir, "allowed-diff-output")
372+
commit.diff(output=allowed_target, allow_unsafe_options=True)
373+
self.assertTrue(osp.isfile(allowed_target))
374+
355375
def test_diff_interface(self):
356376
"""Test a few variations of the main diff routine."""
357377
assertion_map = {}

0 commit comments

Comments
 (0)