diff --git a/Lib/http/cookiejar.py b/Lib/http/cookiejar.py index 5c5dc78b064885..03c8352705c624 100644 --- a/Lib/http/cookiejar.py +++ b/Lib/http/cookiejar.py @@ -342,9 +342,11 @@ def unmatched(match): start, end = match.span(0) return match.string[:start]+match.string[end:] -HEADER_TOKEN_RE = re.compile(r"^\s*([^=\s;,]+)") -HEADER_QUOTED_VALUE_RE = re.compile(r"^\s*=\s*\"([^\"\\]*(?:\\.[^\"\\]*)*)\"") -HEADER_VALUE_RE = re.compile(r"^\s*=\s*([^\s;,]*)") +HEADER_TOKEN_RE = re.compile(r"\s*([^=\s;,]+)") +HEADER_QUOTED_VALUE_RE = re.compile(r"\s*=\s*\"([^\"\\]*(?:\\.[^\"\\]*)*)\"") +HEADER_VALUE_RE = re.compile(r"\s*=\s*([^\s;,]*)") +HEADER_COMMA_RE = re.compile(r"\s*,") +HEADER_JUNK_RE = re.compile(r"[=\s;]*") HEADER_ESCAPE_RE = re.compile(r"\\(.)") def split_header_words(header_values): r"""Parse header values into a list of lists containing key,value pairs. @@ -394,40 +396,40 @@ def split_header_words(header_values): assert not isinstance(header_values, str) result = [] for text in header_values: - orig_text = text pairs = [] - while text: - m = HEADER_TOKEN_RE.search(text) + pos = 0 + while pos < len(text): + m = HEADER_TOKEN_RE.match(text, pos) if m: - text = unmatched(m) + pos = m.end() name = m.group(1) - m = HEADER_QUOTED_VALUE_RE.search(text) + m = HEADER_QUOTED_VALUE_RE.match(text, pos) if m: # quoted value - text = unmatched(m) + pos = m.end() value = m.group(1) value = HEADER_ESCAPE_RE.sub(r"\1", value) else: - m = HEADER_VALUE_RE.search(text) + m = HEADER_VALUE_RE.match(text, pos) if m: # unquoted value - text = unmatched(m) + pos = m.end() value = m.group(1) value = value.rstrip() else: # no value, a lone token value = None pairs.append((name, value)) - elif text.lstrip().startswith(","): + elif m := HEADER_COMMA_RE.match(text, pos): # concatenated headers, as per RFC 2616 section 4.2 - text = text.lstrip()[1:] + pos = m.end() if pairs: result.append(pairs) pairs = [] else: # skip junk - non_junk, nr_junk_chars = re.subn(r"^[=\s;]*", "", text) - assert nr_junk_chars > 0, ( + m = HEADER_JUNK_RE.match(text, pos) + assert m.end() > pos, ( "split_header_words bug: '%s', '%s', %s" % - (orig_text, text, pairs)) - text = non_junk + (text, text[pos:], pairs)) + pos = m.end() if pairs: result.append(pairs) return result diff --git a/Lib/test/test_http_cookiejar.py b/Lib/test/test_http_cookiejar.py index 218181f1185365..1e37b5b4c58bbb 100644 --- a/Lib/test/test_http_cookiejar.py +++ b/Lib/test/test_http_cookiejar.py @@ -267,6 +267,12 @@ def test_split_header_words(self, arg, expect): Got: '%s' """ % (arg, expect, result)) + @support.requires_resource('cpu') + def test_split_header_words_large(self): + n = 10**6 + result = split_header_words(['a=1; b="2", ' * n]) + self.assertEqual(result, [[('a', '1'), ('b', '2')]] * n) + @support.subTests('arg,expect', [ ("foo", "foo"), ("foo=bar", "foo=bar"), diff --git a/Misc/NEWS.d/next/Library/2026-10-17-19-38-11.gh-issue-159642.bvEabvCZXf.rst b/Misc/NEWS.d/next/Library/2026-10-17-19-38-11.gh-issue-159642.bvEabvCZXf.rst new file mode 100644 index 00000000000000..ef89b488d336d9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-17-19-38-11.gh-issue-159642.bvEabvCZXf.rst @@ -0,0 +1,3 @@ +Fix quadratic complexity in :func:`!http.cookiejar.split_header_words`, +which is used by :class:`http.cookiejar.CookieJar` to parse ``Set-Cookie2`` +headers.