Skip to content

Commit c6cb407

Browse files
authored
Merge pull request #2290 from Keerthana-64/tag-header-line-feeds
fix(tag): split tag object headers at line feeds only
2 parents 108935d + add9920 commit c6cb407

2 files changed

Lines changed: 37 additions & 1 deletion

File tree

‎git/objects/tag.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,16 @@ def _set_cache_(self, attr: str) -> None:
106106
"""Cache all our attributes at once."""
107107
if attr in TagObject.__slots__:
108108
ostream = self.repo.odb.stream(self.binsha)
109-
lines: List[str] = ostream.read().decode(defenc, "replace").splitlines()
109+
# Git ends a header only at a line feed, but str.splitlines() also
110+
# breaks at "\r", "\v", "\f", U+2028 and similar. Splitting a tag
111+
# object that way lets an untrusted "tag" header containing such a
112+
# character shift every later header by a line. Split at "\n" only,
113+
# and drop the trailing empty element so tags that use line feeds
114+
# parse exactly as before.
115+
data = ostream.read().decode(defenc, "replace")
116+
lines: List[str] = data.split("\n")
117+
if lines and lines[-1] == "":
118+
lines.pop()
110119

111120
_obj, hexsha = lines[0].split(" ")
112121
_type_token, type_name = lines[1].split(" ")

‎test/test_refs.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
import os.path as osp
88
import tempfile
99
import unittest
10+
from io import BytesIO
1011
from itertools import chain
1112
from pathlib import Path
1213
from unittest.mock import patch
1314

15+
from gitdb import IStream
1416
from gitdb.exc import BadName
1517

1618
import git.refs as refs
@@ -139,6 +141,31 @@ def test_tags_author(self):
139141
tagger_name = tagobj.tagger.name
140142
assert tagger_name == "Michael Trier"
141143

144+
def test_tag_headers_split_at_line_feeds_only(self):
145+
# A "tag" header value carrying a U+2028 (or any separator that
146+
# str.splitlines() honors but git does not) must not shift the later
147+
# headers. Git ends a header only at "\n", so GitPython must read the
148+
# whole value as the tag name and the following line as the tagger,
149+
# exactly as "git cat-file tag" does. See the TagObject parsing fix.
150+
with tempfile.TemporaryDirectory() as tmp_dir:
151+
with self._repo_with_initial_commit(Path(tmp_dir)) as repo:
152+
commit_hexsha = repo.head.commit.hexsha
153+
payload = (
154+
f"object {commit_hexsha}\n"
155+
"type commit\n"
156+
"tag v1
tagger Linus Torvalds <torvalds@linux-foundation.org> 1700000000 +0000\n"
157+
"tagger Mallory <m@evil.example> 1700000001 +0000\n"
158+
"\n"
159+
"release\n"
160+
).encode("utf-8")
161+
istream = repo.odb.store(IStream("tag", len(payload), BytesIO(payload)))
162+
tagobj = TagObject(repo, istream.binsha)
163+
164+
assert tagobj.tag == "v1
tagger Linus Torvalds <torvalds@linux-foundation.org> 1700000000 +0000"
165+
assert tagobj.tagger.name == "Mallory"
166+
assert tagobj.tagger.email == "m@evil.example"
167+
assert tagobj.message == "release"
168+
142169
def test_tags(self):
143170
# Tag refs can point to tag objects or to commits.
144171
s = set()

0 commit comments

Comments
 (0)