From 3ecd05252df7c043d077a8c7ecaa573465e0cc8a Mon Sep 17 00:00:00 2001
From: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Date: Fri, 15 Mar 2024 12:06:12 -0700
Subject: [PATCH ]  CVE-2024-21503
Fix catastrophic performance in lines_with_leading_tabs_expanded() (#4278)

---
 src/black/strings.py | 18 ++++++------------
 tests/test_black.py  | 11 +++++++++++
 2 files changed, 17 insertions(+), 12 deletions(-)

diff --git a/src/black/strings.py b/src/black/strings.py
index 0e0f968..baa8816 100644
--- a/src/black/strings.py
+++ b/src/black/strings.py
@@ -14,7 +14,6 @@ STRING_PREFIX_CHARS: Final = "furbFURB"  # All possible string prefix characters
 STRING_PREFIX_RE: Final = re.compile(
     r"^([" + STRING_PREFIX_CHARS + r"]*)(.*)$", re.DOTALL
 )
-FIRST_NON_WHITESPACE_RE: Final = re.compile(r"\s*\t+\s*(\S)")
 UNICODE_ESCAPE_RE: Final = re.compile(
     r"(?P<backslashes>\\+)(?P<body>"
     r"(u(?P<u>[a-fA-F0-9]{4}))"  # Character with 16-bit hex value xxxx
@@ -51,18 +50,13 @@ def lines_with_leading_tabs_expanded(s: str) -> List[str]:
     """
     lines = []
     for line in s.splitlines():
-        # Find the index of the first non-whitespace character after a string of
-        # whitespace that includes at least one tab
-        match = FIRST_NON_WHITESPACE_RE.match(line)
-        if match:
-            first_non_whitespace_idx = match.start(1)
-
-            lines.append(
-                line[:first_non_whitespace_idx].expandtabs()
-                + line[first_non_whitespace_idx:]
-            )
-        else:
+        stripped_line = line.lstrip()
+        if not stripped_line or stripped_line == line:
             lines.append(line)
+        else:
+            prefix_length = len(line) - len(stripped_line)
+            prefix = line[:prefix_length].expandtabs()
+            lines.append(prefix + stripped_line)
     if s.endswith("\n"):
         lines.append("")
     return lines
diff --git a/tests/test_black.py b/tests/test_black.py
index 41f87cd..1814fb7 100644
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -47,6 +47,7 @@ from black.debug import DebugVisitor
 from black.mode import Mode, Preview
 from black.output import color_diff, diff
 from black.report import Report
+from black.strings import lines_with_leading_tabs_expanded
 
 # Import other test classes
 from tests.util import (
@@ -2054,6 +2055,16 @@ class BlackTestCase(BlackBaseTestCase):
             b"Cannot use line-ranges in the pyproject.toml file." in result.stderr_bytes
         )
 
+    def test_lines_with_leading_tabs_expanded(self) -> None:
+        # See CVE-2024-21503. Mostly test that this completes in a reasonable
+        # time.
+        payload = "\t" * 10_000
+        assert lines_with_leading_tabs_expanded(payload) == [payload]
+
+        tab = " " * 8
+        assert lines_with_leading_tabs_expanded("\tx") == [f"{tab}x"]
+        assert lines_with_leading_tabs_expanded("\t\tx") == [f"{tab}{tab}x"]
+        assert lines_with_leading_tabs_expanded("\tx\n  y") == [f"{tab}x", "  y"]
 
 class TestCaching:
     def test_get_cache_dir(
-- 
2.37.2.windows.2

