From 6c29d2ac15ca8848cd60c68bd36e592cfc028fda Mon Sep 17 00:00:00 2001
From: Seth Larson <seth@python.org>
Date: Tue, 23 Jun 2026 08:33:51 -0500
Subject: [PATCH] gh-143927: Normalize all line endings (CR, CRLF, and LF) in
 configparser (GH-143929) (cherry picked from commit
 5858e42c539dac8394636a6e9b30472b8994851f)

Co-authored-by: Seth Larson <seth@python.org>
---
 Lib/configparser.py                                                      |    4 ++-
 Lib/test/test_configparser.py                                            |   11 ++++++++++
 Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst |    2 +
 3 files changed, 16 insertions(+), 1 deletion(-)
 create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst

Index: Python-3.11.15/Lib/configparser.py
===================================================================
--- Python-3.11.15.orig/Lib/configparser.py	2026-07-31 16:24:24.390829900 +0200
+++ Python-3.11.15/Lib/configparser.py	2026-07-31 16:24:30.769723267 +0200
@@ -945,7 +945,9 @@
             value = self._interpolation.before_write(self, section_name, key,
                                                      value)
             if value is not None or not self._allow_no_value:
-                value = delimiter + str(value).replace('\n', '\n\t')
+                # Convert all possible line-endings into '\n\t'
+                value = (delimiter + str(value).replace('\r\n', '\n')
+                         .replace('\r', '\n').replace('\n', '\n\t'))
             else:
                 value = ""
             fp.write("{}{}\n".format(key, value))
Index: Python-3.11.15/Lib/test/test_configparser.py
===================================================================
--- Python-3.11.15.orig/Lib/test/test_configparser.py	2026-07-31 16:24:26.518618369 +0200
+++ Python-3.11.15/Lib/test/test_configparser.py	2026-07-31 16:24:30.770159190 +0200
@@ -528,6 +528,17 @@
             cf.get(self.default_section, "Foo"), "Bar",
             "could not locate option, expecting case-insensitive defaults")
 
+    def test_crlf_normalization(self):
+        cf = self.newconfig({"key1": "a\nb","key2": "a\rb", "key3": "a\r\nb", "key4": "a\r\nb"})
+        buf = io.StringIO()
+        cf.write(buf)
+        cf_str = buf.getvalue()
+        self.assertNotIn("\r", cf_str)
+        self.assertNotIn("\r\n", cf_str)
+        self.assertEqual(cf_str.count("\n"), 10)
+        self.assertEqual(cf_str.count("\n\t"), 4)
+        self.assertTrue(cf_str.endswith("\n\n"))
+
     def test_parse_errors(self):
         cf = self.newconfig()
         self.parse_error(cf, configparser.ParsingError,
Index: Python-3.11.15/Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ Python-3.11.15/Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst	2026-07-31 16:24:30.770453896 +0200
@@ -0,0 +1,2 @@
+Normalize all line endings (CR, CRLF, and LF) to LF+TAB when writing
+multi-line configparser values.
