CVE-2024-4340: Uncontrolled Recursion (CWE-674)
value: Passing a heavily nested list to sqlparse.parse() leads to a Denial of Service due to RecursionError.


CWE description: The product does not properly control the amount of recursion that takes place,  consuming excessive resources, such as allocated memory or the program stack.
Published: 2024-04-30T15:15Z
Repository: https://github.com/andialbrecht/sqlparse
Language: Python
Files changed: CHANGELOG, sqlparse/sql.py, tests/test_regressions.py
Commit date: 2024-04-13 14:01:50 +0200
Commit message: Raise SQLParseError instead of RecursionError.

Diff: diff --git a/CHANGELOG b/CHANGELOG
index 4e98e7f6..6c442c05 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,10 +3,15 @@ Development Version
 
 Notable Changes
 
 * Drop support for Python 3.5, 3.6, and 3.7.
 * Python 3.12 is now supported (pr725, by hugovk).
+* IMPORTANT: Fixes a potential denial of service attack (DOS) due to recursion
+  error for deeply nested statements. Instead of recursion error a generic
+  SQLParseError is raised. See the security advisory for details:
+  https://github.com/andialbrecht/sqlparse/security/advisories/GHSA-2m57-hf25-phgg
+  The vulnerability was discovered by @uriyay-jfrog. Thanks for reporting!
 
 Enhancements:
 
 * Splitting statements now allows to remove the semicolon at the end.
   Some database backends love statements without semicolon (issue742).
diff --git a/sqlparse/sql.py b/sqlparse/sql.py
index 05e17748..bd5f35b1 100644
--- a/sqlparse/sql.py
+++ b/sqlparse/sql.py
@@ -8,10 +8,11 @@
 """This module contains classes representing syntactical elements of SQL."""
 
 import re
 
 from sqlparse import tokens as T
+from sqlparse.exceptions import SQLParseError
 from sqlparse.utils import imt, remove_quotes
 
 
 class NameAliasMixin:
     """Implements get_real_name and get_alias."""
@@ -207,15 +208,18 @@ class TokenList(Token):
     def flatten(self):
         """Generator yielding ungrouped tokens.
 
         This method is recursively called for all child tokens.
         """
-        for token in self.tokens:
-            if token.is_group:
-                yield from token.flatten()
-            else:
-                yield token
+        try:
+            for token in self.tokens:
+                if token.is_group:
+                    yield from token.flatten()
+                else:
+                    yield token
+        except RecursionError as err:
+            raise SQLParseError('Maximum recursion depth exceeded') from err
 
     def get_sublists(self):
         for token in self.tokens:
             if token.is_group:
                 yield token
diff --git a/tests/test_regressions.py b/tests/test_regressions.py
index 29cb502c..1edd3da6 100644
--- a/tests/test_regressions.py
+++ b/tests/test_regressions.py
@@ -1,11 +1,13 @@
 import copy
+import sys
 
 import pytest
 
 import sqlparse
 from sqlparse import sql, tokens as T
+from sqlparse.exceptions import SQLParseError
 
 
 def test_issue9():
     # make sure where doesn't consume parenthesis
     p = sqlparse.parse('(where 1)')[0]
@@ -447,6 +449,19 @@ def test_copy_issue672():
 
 
 def test_primary_key_issue740():
     p = sqlparse.parse('PRIMARY KEY')[0]
     assert len(p.tokens) == 1
-    assert p.tokens[0].ttype == T.Keyword
\ No newline at end of file
+    assert p.tokens[0].ttype == T.Keyword
+
+
+@pytest.fixture
+def limit_recursion():
+    curr_limit = sys.getrecursionlimit()
+    sys.setrecursionlimit(70)
+    yield
+    sys.setrecursionlimit(curr_limit)
+
+
+def test_max_recursion(limit_recursion):
+    with pytest.raises(SQLParseError):
+        sqlparse.parse('[' * 100 + ']' * 100)

Vulnerable code:         for token in self.tokens:
            if token.is_group:
                yield from token.flatten()
            else:
                yield token
    assert p.tokens[0].ttype == T.Keyword
Fixed code: * IMPORTANT: Fixes a potential denial of service attack (DOS) due to recursion
  error for deeply nested statements. Instead of recursion error a generic
  SQLParseError is raised. See the security advisory for details:
  https://github.com/andialbrecht/sqlparse/security/advisories/GHSA-2m57-hf25-phgg
  The vulnerability was discovered by @uriyay-jfrog. Thanks for reporting!
from sqlparse.exceptions import SQLParseError
        try:
            for token in self.tokens:
                if token.is_group:
                    yield from token.flatten()
                else:
                    yield token
        except RecursionError as err:
            raise SQLParseError('Maximum recursion depth exceeded') from err
import sys
from sqlparse.exceptions import SQLParseError
    assert p.tokens[0].ttype == T.Keyword


@pytest.fixture
def limit_recursion():
    curr_limit = sys.getrecursionlimit()
    sys.setrecursionlimit(70)
    yield
    sys.setrecursionlimit(curr_limit)


def test_max_recursion(limit_recursion):
    with pytest.raises(SQLParseError):
        sqlparse.parse('[' * 100 + ']' * 100)