Skip to content

Commit 040de4b

Browse files
codexByron
authored andcommitted
fix: guard diff output options
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.
1 parent 19799d0 commit 040de4b

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)