new file mode 100644
@@ -0,0 +1,147 @@
+From ca01c323483883f205ee7bb44581c674fcfe6e49 Mon Sep 17 00:00:00 2001
+From: Andi Albrecht <albrecht.andi@gmail.com>
+Date: Mon, 10 Aug 2026 07:35:42 +0200
+Subject: [PATCH] Fix quadratic DoS in group_comments (GHSA-f2ff-p2ww-7p4p)
+
+A comment-only statement ('-- c\n' repeated) made group_comments rescan
+the whole remaining token tail once per comment token, costing O(n^2).
+Because group_comments runs before the MAX_GROUPING_TOKENS guard, the
+cost was paid even on oversized input.
+
+Stop as soon as token_not_matching finds no terminator in the remaining
+tokens: from that point on nothing can group, so re-scanning the tail is
+wasted work. This makes the pass O(n) while preserving grouping output.
+
+Add benchmarks/validate_group_comments_dos.py to check the scaling.
+
+Reported by sanktjodel.
+
+Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
+
+(cherry picked from commit ef2012a5eeb491e604dea2b00d516904a3830c87)
+
+CVE: CVE-2026-71491
+Upstream-Status: Backport [https://github.com/andialbrecht/sqlparse/commit/ef2012a5eeb491e604dea2b00d516904a3830c87]
+
+Dropped changes to the CHANGELOG file.
+
+Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
+---
+ benchmarks/validate_group_comments_dos.py | 85 +++++++++++++++++++++++
+ sqlparse/engine/grouping.py | 11 ++-
+ 2 files changed, 93 insertions(+), 3 deletions(-)
+ create mode 100644 benchmarks/validate_group_comments_dos.py
+
+diff --git a/benchmarks/validate_group_comments_dos.py b/benchmarks/validate_group_comments_dos.py
+new file mode 100644
+index 0000000..58b3e01
+--- /dev/null
++++ b/benchmarks/validate_group_comments_dos.py
+@@ -0,0 +1,85 @@
++"""Validate that ``group_comments`` scales linearly on comment-only input.
++
++Regression check for the quadratic O(n^2) DoS in ``group_comments``
++(sqlparse/engine/grouping.py), reported as GHSA-f2ff-p2ww-7p4p.
++
++A statement made only of single-line comments (``'-- c\\n'`` repeated n times)
++lexes in O(n) but ``group_comments`` rescans the O(n) remaining tokens for every
++comment token, giving O(n^2) total work. ``group_comments`` runs first in
++``group()``, before the ``MAX_GROUPING_TOKENS`` guard, so the token cap does not
++protect this vector. The path is reachable via ``sqlparse.parse()`` and
++``sqlparse.format(sql, strip_comments=True)``.
++
++This script measures the scaling of the vulnerable path and reports whether the
++observed growth is quadratic (vulnerable) or roughly linear (patched).
++
++Run with: python benchmarks/validate_group_comments_dos.py
++
++Exit code 0 => behaviour looks linear (advisory mitigated).
++Exit code 1 => behaviour looks quadratic (advisory reproduced).
++"""
++
++import sys
++import time
++
++import sqlparse
++
++
++def payload(n):
++ """A comment-only statement of n single-line comments."""
++ return '-- c\n' * n
++
++
++def measure(fn, sql):
++ t0 = time.perf_counter()
++ fn(sql)
++ return (time.perf_counter() - t0) * 1000
++
++
++def run(label, fn):
++ print(f'{label}:')
++ sizes = (1000, 2000, 4000, 8000)
++ timings = []
++ for n in sizes:
++ dt = measure(fn, payload(n))
++ timings.append(dt)
++ print(f' n={n:5d} {dt:8.1f} ms ({len(payload(n))} B)')
++
++ # For each doubling of the input, quadratic growth ~4x, linear ~2x.
++ ratios = [b / a for a, b in zip(timings, timings[1:]) if a > 0]
++ print(f' doubling ratios: {", ".join(f"{r:.2f}x" for r in ratios)}')
++ return ratios
++
++
++def classify(ratios):
++ """Quadratic if the average per-doubling ratio is closer to 4x than 2x."""
++ if not ratios:
++ return 'inconclusive', 0.0
++ avg = sum(ratios) / len(ratios)
++ # Midpoint between linear (2x) and quadratic (4x) is 3x.
++ return ('quadratic' if avg >= 3.0 else 'linear'), avg
++
++
++def main():
++ print('GHSA-f2ff-p2ww-7p4p: quadratic DoS in group_comments\n')
++
++ all_ratios = []
++ all_ratios += run('sqlparse.parse', sqlparse.parse)
++ print()
++ all_ratios += run(
++ 'sqlparse.format(strip_comments=True)',
++ lambda s: sqlparse.format(s, strip_comments=True),
++ )
++ print()
++
++ verdict, avg = classify(all_ratios)
++ print(f'Average doubling ratio: {avg:.2f}x => {verdict}')
++ if verdict == 'quadratic':
++ print('VULNERABLE: growth is quadratic, advisory reproduced.')
++ return 1
++ print('OK: growth is roughly linear, advisory mitigated.')
++ return 0
++
++
++if __name__ == '__main__':
++ sys.exit(main())
+diff --git a/sqlparse/engine/grouping.py b/sqlparse/engine/grouping.py
+index 43ca5b5..7a3ca1a 100644
+--- a/sqlparse/engine/grouping.py
++++ b/sqlparse/engine/grouping.py
+@@ -339,9 +339,14 @@ def group_comments(tlist):
+ while token:
+ eidx, end = tlist.token_not_matching(
+ lambda tk: imt(tk, t=T.Comment) or tk.is_newline, idx=tidx)
+- if end is not None:
+- eidx, end = tlist.token_prev(eidx, skip_ws=False)
+- tlist.group_tokens(sql.Comment, tidx, eidx)
++ if end is None:
++ # From tidx onward everything is comment/newline: there is no
++ # terminator to group against, and every later start would hit
++ # the same dead end. Stop instead of re-scanning the tail once
++ # per remaining comment token (which is O(n**2)).
++ break
++ eidx, end = tlist.token_prev(eidx, skip_ws=False)
++ tlist.group_tokens(sql.Comment, tidx, eidx)
+
+ tidx, token = tlist.token_next_by(t=T.Comment, idx=tidx)
+
@@ -10,6 +10,7 @@ SRC_URI += "file://CVE-2026-54284-1.patch \
file://CVE-2026-54284-2.patch \
file://CVE-2026-59893.patch \
file://CVE-2026-59894.patch \
+ file://CVE-2026-71491.patch \
"
CVE_PRODUCT = "sqlparse"