Skip to content

Commit da67ac1

Browse files
committed
Enhance DoS protection by raising SQLParseError for exceeded grouping limits (fixes #827).
Co-authored-by: AI
1 parent 5ca50a2 commit da67ac1

File tree

3 files changed

+29
-28
lines changed

3 files changed

+29
-28
lines changed

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ Development Version
33

44
Bug Fixes
55

6+
* Fix DoS protection to raise SQLParseError instead of silently returning None
7+
when grouping limits are exceeded (issue827).
68
* Fix splitting of BEGIN TRANSACTION statements (issue826).
79

810

sqlparse/engine/grouping.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from sqlparse import sql
99
from sqlparse import tokens as T
10+
from sqlparse.exceptions import SQLParseError
1011
from sqlparse.utils import recurse, imt
1112

1213
# Maximum recursion depth for grouping operations to prevent DoS attacks
@@ -26,12 +27,16 @@
2627
def _group_matching(tlist, cls, depth=0):
2728
"""Groups Tokens that have beginning and end."""
2829
if MAX_GROUPING_DEPTH is not None and depth > MAX_GROUPING_DEPTH:
29-
return
30+
raise SQLParseError(
31+
f"Maximum grouping depth exceeded ({MAX_GROUPING_DEPTH})."
32+
)
3033

3134
# Limit the number of tokens to prevent DoS attacks
3235
if MAX_GROUPING_TOKENS is not None \
3336
and len(tlist.tokens) > MAX_GROUPING_TOKENS:
34-
return
37+
raise SQLParseError(
38+
f"Maximum number of tokens exceeded ({MAX_GROUPING_TOKENS})."
39+
)
3540

3641
opens = []
3742
tidx_offset = 0
@@ -480,12 +485,16 @@ def _group(tlist, cls, match,
480485
):
481486
"""Groups together tokens that are joined by a middle token. i.e. x < y"""
482487
if MAX_GROUPING_DEPTH is not None and depth > MAX_GROUPING_DEPTH:
483-
return
488+
raise SQLParseError(
489+
f"Maximum grouping depth exceeded ({MAX_GROUPING_DEPTH})."
490+
)
484491

485492
# Limit the number of tokens to prevent DoS attacks
486493
if MAX_GROUPING_TOKENS is not None \
487494
and len(tlist.tokens) > MAX_GROUPING_TOKENS:
488-
return
495+
raise SQLParseError(
496+
f"Maximum number of tokens exceeded ({MAX_GROUPING_TOKENS})."
497+
)
489498

490499
tidx_offset = 0
491500
pidx, prev_ = None, None

tests/test_dos_prevention.py

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,53 @@
22

33
import pytest
44
import sqlparse
5+
from sqlparse.exceptions import SQLParseError
56
import time
67

78

89
class TestDoSPrevention:
910
"""Test cases to ensure sqlparse is protected against DoS attacks."""
1011

1112
def test_large_tuple_list_performance(self):
12-
"""Test that parsing a large list of tuples doesn't cause DoS."""
13+
"""Test that parsing a large list of tuples raises SQLParseError."""
1314
# Generate SQL with many tuples (like Django composite primary key queries)
1415
sql = '''
1516
SELECT "composite_pk_comment"."tenant_id", "composite_pk_comment"."comment_id"
1617
FROM "composite_pk_comment"
1718
WHERE ("composite_pk_comment"."tenant_id", "composite_pk_comment"."comment_id") IN ('''
1819

19-
# Generate 5000 tuples - this would previously cause a hang
20+
# Generate 5000 tuples - this should trigger MAX_GROUPING_TOKENS
2021
tuples = []
2122
for i in range(1, 5001):
2223
tuples.append(f"(1, {i})")
2324

2425
sql += ", ".join(tuples) + ")"
2526

26-
# Test should complete quickly (under 5 seconds)
27-
start_time = time.time()
28-
result = sqlparse.format(sql, reindent=True, keyword_case="upper")
29-
execution_time = time.time() - start_time
30-
31-
assert execution_time < 5.0, f"Parsing took too long: {execution_time:.2f}s"
32-
assert len(result) > 0, "Result should not be empty"
33-
assert "SELECT" in result.upper(), "SQL should be properly formatted"
27+
# Should raise SQLParseError due to token limit
28+
with pytest.raises(SQLParseError, match="Maximum number of tokens exceeded"):
29+
sqlparse.format(sql, reindent=True, keyword_case="upper")
3430

3531
def test_deeply_nested_groups_limited(self):
36-
"""Test that deeply nested groups don't cause stack overflow."""
32+
"""Test that deeply nested groups raise SQLParseError."""
3733
# Create deeply nested parentheses
3834
sql = "SELECT " + "(" * 200 + "1" + ")" * 200
3935

40-
# Should not raise RecursionError
41-
result = sqlparse.format(sql, reindent=True)
42-
assert "SELECT" in result
43-
assert "1" in result
36+
# Should raise SQLParseError due to depth limit
37+
with pytest.raises(SQLParseError, match="Maximum grouping depth exceeded"):
38+
sqlparse.format(sql, reindent=True)
4439

4540
def test_very_large_token_list_limited(self):
46-
"""Test that very large token lists are handled gracefully."""
41+
"""Test that very large token lists raise SQLParseError."""
4742
# Create a SQL with many identifiers
4843
identifiers = []
4944
for i in range(15000): # More than MAX_GROUPING_TOKENS
5045
identifiers.append(f"col{i}")
5146

5247
sql = f"SELECT {', '.join(identifiers)} FROM table1"
5348

54-
# Should complete without hanging
55-
start_time = time.time()
56-
result = sqlparse.format(sql, reindent=True)
57-
execution_time = time.time() - start_time
58-
59-
assert execution_time < 10.0, f"Parsing took too long: {execution_time:.2f}s"
60-
assert "SELECT" in result
61-
assert "FROM" in result
49+
# Should raise SQLParseError due to token limit
50+
with pytest.raises(SQLParseError, match="Maximum number of tokens exceeded"):
51+
sqlparse.format(sql, reindent=True)
6252

6353
def test_normal_sql_still_works(self):
6454
"""Test that normal SQL still works correctly after DoS protections."""

0 commit comments

Comments
 (0)