Skip to content

Commit f44c1fb

Browse files
authored
Merge pull request #2215 from gitpython-developers/various-fixes
Harden diff path and actor identity parsing
2 parents 4b9afe9 + 751473a commit f44c1fb

5 files changed

Lines changed: 76 additions & 26 deletions

File tree

doc/source/changes.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22
Changelog
33
=========
44

5+
3.1.60
6+
======
7+
8+
Security fixes for
9+
10+
* https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-g5vv-9gxw-82hx
11+
12+
If you can, also try and provide feedback on the upcoming v4 branch
13+
https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome.
14+
15+
See the following for all changes.
16+
https://github.com/gitpython-developers/GitPython/releases/tag/3.1.60
17+
518
3.1.59
619
======
720

git/diff.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,24 +95,43 @@ class DiffConstants(enum.Enum):
9595
:const:`git.INDEX` and :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`.
9696
"""
9797

98-
_octal_byte_re = re.compile(rb"\\([0-9]{3})")
9998

100-
101-
def _octal_repl(matchobj: Match) -> bytes:
102-
value = matchobj.group(1)
103-
value = int(value, 8)
104-
value = bytes(bytearray((value,)))
105-
return value
99+
def _unquote_path(path: bytes) -> bytes:
100+
result = bytearray()
101+
escapes = {
102+
ord("a"): 7,
103+
ord("b"): 8,
104+
ord("f"): 12,
105+
ord("n"): 10,
106+
ord("r"): 13,
107+
ord("t"): 9,
108+
ord("v"): 11,
109+
}
110+
i = 0
111+
while i < len(path):
112+
if path[i] != ord("\\") or i + 1 == len(path):
113+
result.append(path[i])
114+
i += 1
115+
continue
116+
if path[i + 1] in b"0123" and i + 3 < len(path) and all(c in b"01234567" for c in path[i + 2 : i + 4]):
117+
result.append(int(path[i + 1 : i + 4], 8))
118+
i += 4
119+
continue
120+
escaped = path[i + 1]
121+
if escaped in escapes or escaped in b'\\"':
122+
result.append(escapes.get(escaped, escaped))
123+
else:
124+
result.extend(path[i : i + 2])
125+
i += 2
126+
return bytes(result)
106127

107128

108129
def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]:
109130
if path == b"/dev/null":
110131
return None
111132

112133
if path.startswith(b'"') and path.endswith(b'"'):
113-
path = path[1:-1].replace(b"\\n", b"\n").replace(b"\\t", b"\t").replace(b'\\"', b'"').replace(b"\\\\", b"\\")
114-
115-
path = _octal_byte_re.sub(_octal_repl, path)
134+
path = _unquote_path(path[1:-1])
116135

117136
if has_ab_prefix:
118137
assert path.startswith(b"a/") or path.startswith(b"b/")

git/util.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -858,10 +858,6 @@ class Actor:
858858
committers and authors or anything with a name and an email as mentioned in the git
859859
log entries."""
860860

861-
# PRECOMPILED REGEX
862-
name_only_regex = re.compile(r"<(.*)>")
863-
name_email_regex = re.compile(r"(.*) <(.*?)>")
864-
865861
# ENVIRONMENT VARIABLES
866862
# These are read when creating new commits.
867863
env_author_name = "GIT_AUTHOR_NAME"
@@ -906,18 +902,14 @@ def _from_string(cls, string: str) -> "Actor":
906902
:return:
907903
:class:`Actor`
908904
"""
909-
m = cls.name_email_regex.search(string)
910-
if m:
911-
name, email = m.groups()
912-
return Actor(name, email)
913-
else:
914-
m = cls.name_only_regex.search(string)
915-
if m:
916-
return Actor(m.group(1), None)
917-
# Assume the best and use the whole string as name.
918-
return Actor(string, None)
919-
# END special case name
920-
# END handle name/email matching
905+
line = string.partition("\n")[0]
906+
left_bracket = line.find("<")
907+
right_bracket = line.find(">", left_bracket + 1)
908+
if left_bracket >= 0 and right_bracket >= 0:
909+
return Actor(line[:left_bracket].rstrip(), line[left_bracket + 1 : right_bracket])
910+
911+
# Assume the best and use the whole string as name.
912+
return Actor(string, None)
921913

922914
@classmethod
923915
def _main_actor(

test/test_actor.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ def test_from_string_should_handle_just_name(self):
2727
self.assertEqual("Michael Trier", a.name)
2828
self.assertEqual(None, a.email)
2929

30+
def test_from_string_handles_unterminated_email_without_regex_backtracking(self):
31+
value = "A" * 20_000 + " <unterminated"
32+
actor = Actor._from_string(value)
33+
self.assertNotIn("name_email_regex", vars(Actor))
34+
self.assertEqual(actor, Actor(value, None))
35+
36+
def test_from_string_does_not_parse_across_lines(self):
37+
self.assertEqual(Actor._from_string("x <a>\n y <b>"), Actor("x", "a"))
38+
39+
def test_from_string_uses_git_delimiters(self):
40+
for value, expected in (
41+
("Name <e<mail>", Actor("Name", "e<mail")),
42+
("Name <email>>", Actor("Name", "email")),
43+
("Name<email>", Actor("Name", "email")),
44+
(" <>", Actor("", "")),
45+
("Name <email", Actor("Name <email", None)),
46+
("Name email>", Actor("Name email>", None)),
47+
):
48+
self.assertEqual(Actor._from_string(value), expected)
49+
3050
def test_should_display_representation(self):
3151
a = Actor._from_string("Michael Trier <mtrier@example.com>")
3252
self.assertEqual('<git.Actor "Michael Trier <mtrier@example.com>">', repr(a))

test/test_diff.py

Lines changed: 6 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.diff import decode_path
1718
from git.exc import UnsafeOptionError
1819

1920
from test.lib import StringProcessAdapter, TestBase, fixture, with_rw_directory
@@ -324,6 +325,11 @@ def test_diff_patch_format(self):
324325
Diff._index_from_patch_format(self.rorepo, diff_proc)
325326
# END for each fixture
326327

328+
def test_decode_path_distinguishes_escaped_backslashes_from_octal_bytes(self):
329+
self.assertEqual(decode_path(b'"foo\\\\899bar"', False), b"foo\\899bar")
330+
self.assertEqual(decode_path(b'"foo\\\\123bar"', False), b"foo\\123bar")
331+
self.assertEqual(decode_path(b'"foo\\123bar"', False), b"fooSbar")
332+
327333
def test_diff_with_spaces(self):
328334
data = StringProcessAdapter(fixture("diff_file_with_spaces"))
329335
diff_index = Diff._index_from_patch_format(self.rorepo, data)

0 commit comments

Comments
 (0)