Skip to content

Commit bde8148

Browse files
codexByron
authored andcommitted
fix: preserve multiline config values when writing
GitConfigParser decoded valid multiline values into embedded newlines, but _write() serialized those newlines as indented physical lines. Rewriting an otherwise unchanged config could therefore change its meaning to Git. Serialize resident multiline values with Git-compatible escapes inside a quoted continuation, preserving GitPython read compatibility while keeping each option structurally intact. This addresses GHSA-284h-m62q-gf8w. The regression starts with an inert multiline value, performs an unrelated write, and verifies with both GitPython and git config that it remains one value and does not create another option. Git baseline: config.c parse_value() and write_pair() at cf5497b14c5a escape embedded LF as \\n rather than emitting it as a physical config line. Validation: - pytest -q test/test_config.py - pytest -q - ruff check git/config.py test/test_config.py - ruff format --check git/config.py test/test_config.py
1 parent 9729ed3 commit bde8148

2 files changed

Lines changed: 23 additions & 1 deletion

File tree

git/config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,11 @@ def write_section(name: str, section_dict: _OMD) -> None:
705705
continue
706706

707707
for v in values:
708-
fp.write(("\t%s = %s\n" % (key, self._value_to_string(v).replace("\n", "\n\t"))).encode(defenc))
708+
value = self._value_to_string(v)
709+
if "\n" in value:
710+
value = value.replace("\\", "\\\\").replace('"', '\\"')
711+
value = '"%s\\\n"' % value.replace("\n", "\\n").replace("\t", "\\t").replace("\b", "\\b")
712+
fp.write(("\t%s = %s\n" % (key, value)).encode(defenc))
709713
# END if key is not __name__
710714

711715
# END section writing

test/test_config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io
88
import os
99
import os.path as osp
10+
import subprocess
1011
import sys
1112
from unittest import mock
1213

@@ -150,6 +151,23 @@ def test_config_value_with_trailing_new_line(self):
150151
git_config = GitConfigParser(config_file)
151152
git_config.read() # This should not throw an exception
152153

154+
@with_rw_directory
155+
def test_rewriting_multiline_value_does_not_create_option(self, rw_dir):
156+
config_path = osp.join(rw_dir, "config")
157+
with open(config_path, "wb") as config_file:
158+
config_file.write(b'[core]\n\tzzz = "A\\nhooksPath = ../evil-hooks\\\n"\n')
159+
160+
with GitConfigParser(config_path, read_only=False) as git_config:
161+
self.assertEqual(git_config.get_value("core", "zzz"), "A\nhooksPath = ../evil-hooks")
162+
git_config.set_value("user", "name", "Test User")
163+
164+
with GitConfigParser(config_path, read_only=True) as git_config:
165+
self.assertEqual(git_config.get_value("core", "zzz"), "A\nhooksPath = ../evil-hooks")
166+
self.assertFalse(git_config.has_option("core", "hooksPath"))
167+
self.assertEqual(
168+
subprocess.run(["git", "config", "--file", config_path, "--get", "core.hooksPath"]).returncode, 1
169+
)
170+
153171
@with_rw_directory
154172
def test_set_value_rejects_config_injection(self, rw_dir):
155173
config_path = osp.join(rw_dir, "config")

0 commit comments

Comments
 (0)