diff --git a/git/objects/tag.py b/git/objects/tag.py index e08d6ef10..2dbf79f1b 100644 --- a/git/objects/tag.py +++ b/git/objects/tag.py @@ -106,7 +106,16 @@ def _set_cache_(self, attr: str) -> None: """Cache all our attributes at once.""" if attr in TagObject.__slots__: ostream = self.repo.odb.stream(self.binsha) - lines: List[str] = ostream.read().decode(defenc, "replace").splitlines() + # Git ends a header only at a line feed, but str.splitlines() also + # breaks at "\r", "\v", "\f", U+2028 and similar. Splitting a tag + # object that way lets an untrusted "tag" header containing such a + # character shift every later header by a line. Split at "\n" only, + # and drop the trailing empty element so tags that use line feeds + # parse exactly as before. + data = ostream.read().decode(defenc, "replace") + lines: List[str] = data.split("\n") + if lines and lines[-1] == "": + lines.pop() _obj, hexsha = lines[0].split(" ") _type_token, type_name = lines[1].split(" ") diff --git a/test/test_refs.py b/test/test_refs.py index 2edb7b766..42bf5200e 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -7,10 +7,12 @@ import os.path as osp import tempfile import unittest +from io import BytesIO from itertools import chain from pathlib import Path from unittest.mock import patch +from gitdb import IStream from gitdb.exc import BadName import git.refs as refs @@ -139,6 +141,31 @@ def test_tags_author(self): tagger_name = tagobj.tagger.name assert tagger_name == "Michael Trier" + def test_tag_headers_split_at_line_feeds_only(self): + # A "tag" header value carrying a U+2028 (or any separator that + # str.splitlines() honors but git does not) must not shift the later + # headers. Git ends a header only at "\n", so GitPython must read the + # whole value as the tag name and the following line as the tagger, + # exactly as "git cat-file tag" does. See the TagObject parsing fix. + with tempfile.TemporaryDirectory() as tmp_dir: + with self._repo_with_initial_commit(Path(tmp_dir)) as repo: + commit_hexsha = repo.head.commit.hexsha + payload = ( + f"object {commit_hexsha}\n" + "type commit\n" + "tag v1
tagger Linus Torvalds 1700000000 +0000\n" + "tagger Mallory 1700000001 +0000\n" + "\n" + "release\n" + ).encode("utf-8") + istream = repo.odb.store(IStream("tag", len(payload), BytesIO(payload))) + tagobj = TagObject(repo, istream.binsha) + + assert tagobj.tag == "v1
tagger Linus Torvalds 1700000000 +0000" + assert tagobj.tagger.name == "Mallory" + assert tagobj.tagger.email == "m@evil.example" + assert tagobj.message == "release" + def test_tags(self): # Tag refs can point to tag objects or to commits. s = set()