CVE-2022-40023: Inefficient Regular Expression Complexity (CWE-1333)
value: Sqlalchemy mako before 1.2.2 is vulnerable to Regular expression Denial of Service when using the Lexer class to parse. This also affects babelplugin and linguaplugin.
CWE description: The product uses a regular expression with an inefficient, possibly exponential worst-case computational complexity that consumes excessive CPU cycles.
CVSS v3 base score: 7.5
Published: 2022-09-07T13:15Z
Repository: https://github.com/sqlalchemy/mako
Language: Python
Files changed: doc/build/unreleased/366.rst, mako/lexer.py, test/test_lexer.py
Commit date: 2022-08-29 12:31:58 -0400
Commit message: fix tag regexp to match quoted groups correctly

Fixed issue in lexer where the regexp used to match tags would not
correctly interpret quoted sections individually. While this parsing issue
still produced the same expected tag structure later on, the mis-handling
of quoted sections was also subject to a regexp crash if a tag had a large
number of quotes within its quoted sections.

Fixes: #366
Change-Id: I74e0d71ff7f419970711a7cd51adcf1bb90a44c0

Diff: diff --git a/doc/build/unreleased/366.rst b/doc/build/unreleased/366.rst
new file mode 100644
index 0000000..27b0278
--- /dev/null
+++ b/doc/build/unreleased/366.rst
@@ -0,0 +1,9 @@
+.. change::
+    :tags: bug, lexer
+    :tickets: 366
+
+    Fixed issue in lexer where the regexp used to match tags would not
+    correctly interpret quoted sections individually. While this parsing issue
+    still produced the same expected tag structure later on, the mis-handling
+    of quoted sections was also subject to a regexp crash if a tag had a large
+    number of quotes within its quoted sections.
\ No newline at end of file
diff --git a/mako/lexer.py b/mako/lexer.py
index bfcf286..77a2483 100644
--- a/mako/lexer.py
+++ b/mako/lexer.py
@@ -270,24 +270,28 @@ class Lexer:
                 self.filename,
             )
         return self.template
 
     def match_tag_start(self):
-        match = self.match(
-            r"""
+        reg = r"""
             \<%     # opening tag
 
             ([\w\.\:]+)   # keyword
 
-            ((?:\s+\w+|\s*=\s*|".*?"|'.*?')*)  # attrname, = \
+            ((?:\s+\w+|\s*=\s*|"[^"]*?"|'[^']*?'|\s*,\s*)*)  # attrname, = \
                                                #        sign, string expression
+                                               # comma is for backwards compat
+                                               # identified in #366
 
             \s*     # more whitespace
 
             (/)?>   # closing
 
-            """,
+        """
+
+        match = self.match(
+            reg,
             re.I | re.S | re.X,
         )
 
         if not match:
             return False
diff --git a/test/test_lexer.py b/test/test_lexer.py
index 255c128..a7b6fe3 100644
--- a/test/test_lexer.py
+++ b/test/test_lexer.py
@@ -1,7 +1,9 @@
 import re
 
+import pytest
+
 from mako import compat
 from mako import exceptions
 from mako import parsetree
 from mako import util
 from mako.lexer import Lexer
@@ -144,10 +146,14 @@ class LexerTest(TemplateTest):
         template = """
             <%namespace name="${foo}"/>
         """
         assert_raises(exceptions.CompileException, Lexer(template).parse)
 
+    def test_tag_many_quotes(self):
+        template = "<%0" + '"' * 3000
+        assert_raises(exceptions.SyntaxException, Lexer(template).parse)
+
     def test_unmatched_tag(self):
         template = """
         <%namespace name="bar">
         <%def name="foo()">
             foo
@@ -430,13 +436,20 @@ class LexerTest(TemplateTest):
                     Text("\n        ", (3, 64)),
                 ],
             ),
         )
 
-    def test_pagetag(self):
-        template = """
-            <%page cached="True", args="a, b"/>
+    @pytest.mark.parametrize("comma,numchars", [(",", 48), ("", 47)])
+    def test_pagetag(self, comma, numchars):
+        # note that the comma here looks like:
+        # <%page cached="True", args="a, b"/>
+        # that's what this test has looked like for decades, however, the
+        # comma there is not actually the right syntax.  When issue #366
+        # was fixed, the reg was altered to accommodate for this comma to allow
+        # backwards compat
+        template = f"""
+            <%page cached="True"{comma} args="a, b"/>
 
             some template
         """
         nodes = Lexer(template).parse()
         self._compare(
@@ -451,11 +464,11 @@ class LexerTest(TemplateTest):
                     Text(
                         """
 
             some template
         """,
-                        (2, 48),
+                        (2, numchars),
                     ),
                 ],
             ),
         )
 

Vulnerable code:         match = self.match(
            r"""
            ((?:\s+\w+|\s*=\s*|".*?"|'.*?')*)  # attrname, = \
            """,
    def test_pagetag(self):
        template = """
            <%page cached="True", args="a, b"/>
                        (2, 48),
Fixed code: .. change::
    :tags: bug, lexer
    :tickets: 366

    Fixed issue in lexer where the regexp used to match tags would not
    correctly interpret quoted sections individually. While this parsing issue
    still produced the same expected tag structure later on, the mis-handling
    of quoted sections was also subject to a regexp crash if a tag had a large
    number of quotes within its quoted sections.
        reg = r"""
            ((?:\s+\w+|\s*=\s*|"[^"]*?"|'[^']*?'|\s*,\s*)*)  # attrname, = \
                                               # comma is for backwards compat
                                               # identified in #366
        """

        match = self.match(
            reg,
import pytest

    def test_tag_many_quotes(self):
        template = "<%0" + '"' * 3000
        assert_raises(exceptions.SyntaxException, Lexer(template).parse)

    @pytest.mark.parametrize("comma,numchars", [(",", 48), ("", 47)])
    def test_pagetag(self, comma, numchars):
        # note that the comma here looks like:
        # <%page cached="True", args="a, b"/>
        # that's what this test has looked like for decades, however, the
        # comma there is not actually the right syntax.  When issue #366
        # was fixed, the reg was altered to accommodate for this comma to allow
        # backwards compat
        template = f"""
            <%page cached="True"{comma} args="a, b"/>
                        (2, numchars),