From f71ce97db92d7f22542271bd49bdba392b27792e Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Tue, 30 Jun 2026 10:54:16 +0200 Subject: [PATCH] [3.14] gh-152248: Reject a POSIX TZ abbreviation with non-ASCII-letters in pure-Python `zoneinfo` (GH-152249) (#152651) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit (cherry picked from commit 449122ed0dbdb6a545af4927c59f4c80ee15c515) Co-authored-by: tonghuaroot (童话) Co-authored-by: Stan Ulbrych --- Lib/test/test_zoneinfo/test_zoneinfo.py | 20 +++++++++++++++++-- Lib/zoneinfo/_zoneinfo.py | 4 ++-- ...-06-26-13-39-11.gh-issue-152248.N2Rmaf.rst | 3 +++ Modules/_zoneinfo.c | 3 +++ 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-26-13-39-11.gh-issue-152248.N2Rmaf.rst diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index 0769f90786a1..b37ab0cd02df 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -1009,14 +1009,14 @@ class TZStrTest(ZoneInfoTestBase): cls._tzif_header = bytes(out) - def zone_from_tzstr(self, tzstr): + def zone_from_tzstr(self, tzstr, encoding="ascii"): """Creates a zoneinfo file following a POSIX rule.""" zonefile = io.BytesIO(self._tzif_header) zonefile.seek(0, 2) # Write the footer zonefile.write(b"\x0A") - zonefile.write(tzstr.encode("ascii")) + zonefile.write(tzstr.encode(encoding)) zonefile.write(b"\x0A") zonefile.seek(0) @@ -1150,6 +1150,13 @@ class TZStrTest(ZoneInfoTestBase): "+11", # Unquoted alphanumeric "GMT,M3.2.0/2,M11.1.0/3", # Transition rule but no DST "GMT0+11,M3.2.0/2,M11.1.0/3", # Unquoted alphanumeric in DST + # Unquoted abbreviation with embedded or leading whitespace + "AB C3", + " A B 3", + "AAA4BB B,J60/2,J300/2", # Embedded whitespace in DST + # Empty quoted abbreviation + "<>5", + "AAA4<>,M3.2.0/2,M11.1.0/3", "PST8PDT,M3.2.0/2", # Only one transition rule # Invalid offset hours "AAA168", @@ -1232,6 +1239,15 @@ class TZStrTest(ZoneInfoTestBase): with self.assertRaisesRegex(ValueError, tzstr_regex): self.zone_from_tzstr(invalid_tzstr) + def test_invalid_tzstr_non_ascii_abbr(self): + tzstr = "ABÀC3" + if self.module is py_zoneinfo: + expected = re.escape(tzstr) + else: + expected = re.escape(repr(tzstr.encode("utf-8"))) + with self.assertRaisesRegex(ValueError, expected): + self.zone_from_tzstr(tzstr, encoding="utf-8") + @classmethod def _populate_test_cases(cls): # This method uses a somewhat unusual style in that it populates the diff --git a/Lib/zoneinfo/_zoneinfo.py b/Lib/zoneinfo/_zoneinfo.py index 52832f600c30..90cf2bbf8f5d 100644 --- a/Lib/zoneinfo/_zoneinfo.py +++ b/Lib/zoneinfo/_zoneinfo.py @@ -640,11 +640,11 @@ def _parse_tz_str(tz_str): parser_re = re.compile( r""" - (?P[^<0-9:.+-]+|<[a-zA-Z0-9+-]+>) + (?P[a-zA-Z]+|<[a-zA-Z0-9+-]+>) (?: (?P[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?) (?: - (?P[^0-9:.+-]+|<[a-zA-Z0-9+-]+>) + (?P[a-zA-Z]+|<[a-zA-Z0-9+-]+>) (?P[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?)? )? # dst )? # stdoff diff --git a/Misc/NEWS.d/next/Library/2026-06-26-13-39-11.gh-issue-152248.N2Rmaf.rst b/Misc/NEWS.d/next/Library/2026-06-26-13-39-11.gh-issue-152248.N2Rmaf.rst new file mode 100644 index 000000000000..8921ad17349e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-26-13-39-11.gh-issue-152248.N2Rmaf.rst @@ -0,0 +1,3 @@ +Make the C and pure-Python :mod:`zoneinfo` parsers validate POSIX TZ +abbreviations consistently, rejecting unquoted abbreviations with non-letter +characters and empty quoted abbreviations. Patch by tonghuaroot. diff --git a/Modules/_zoneinfo.c b/Modules/_zoneinfo.c index 8dcf4252956d..f3fb5127773f 100644 --- a/Modules/_zoneinfo.c +++ b/Modules/_zoneinfo.c @@ -1759,6 +1759,9 @@ parse_abbr(const char **p, PyObject **abbr) ptr++; } str_end = ptr; + if (str_end == str_start) { + return -1; + } ptr++; } else { -- 2.47.3