From 2fa32e5e270ab47029aea05c6e3e970d54513cb3 Mon Sep 17 00:00:00 2001 From: Ankur Tyagi Date: Mon, 14 Sep 2026 15:12:55 +1200 Subject: [PATCH] python3-sqlparse: patch CVE-2026-59893 Details: https://nvd.nist.gov/vuln/detail/cve-2026-59893 Signed-off-by: Ankur Tyagi Signed-off-by: Anuj Mittal --- .../python3-sqlparse/CVE-2026-59893.patch | 293 ++++++++++++++++++ .../python/python3-sqlparse_0.5.5.bb | 1 + 2 files changed, 294 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-sqlparse/CVE-2026-59893.patch diff --git a/meta-python/recipes-devtools/python/python3-sqlparse/CVE-2026-59893.patch b/meta-python/recipes-devtools/python/python3-sqlparse/CVE-2026-59893.patch new file mode 100644 index 0000000000..39dd978266 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-sqlparse/CVE-2026-59893.patch @@ -0,0 +1,293 @@ +From 3cca1f008a24b9817137d020eeed87efa5ed68d3 Mon Sep 17 00:00:00 2001 +From: Andi Albrecht +Date: Wed, 1 Jul 2026 08:38:53 +0200 +Subject: [PATCH] Fix uncontrolled CPU consumption (ReDoS) in the lexer's + handling of dollar-quoted literals and multiline comments. + +(cherry picked from commit d1d80602741f77ec78e5a04ce4719244cf32352e) + +CVE: CVE-2026-59893 +Upstream-Status: Backport [https://github.com/andialbrecht/sqlparse/commit/d1d80602741f77ec78e5a04ce4719244cf32352e] + +Dropped changes to the CHANGELOG file. + +Signed-off-by: Ankur Tyagi +--- + benchmarks/bench_dollar_quote_redos.py | 90 ++++++++++++++++++++++++++ + sqlparse/keywords.py | 60 ++++++++++++++++- + sqlparse/lexer.py | 8 +++ + sqlparse/utils.py | 46 ++++++++++++- + 4 files changed, 200 insertions(+), 4 deletions(-) + create mode 100644 benchmarks/bench_dollar_quote_redos.py + +diff --git a/benchmarks/bench_dollar_quote_redos.py b/benchmarks/bench_dollar_quote_redos.py +new file mode 100644 +index 0000000..234d9c4 +--- /dev/null ++++ b/benchmarks/bench_dollar_quote_redos.py +@@ -0,0 +1,90 @@ ++"""Delimited-literal lexer benchmark (GHSA-prg7-hcfm-mfcr). ++ ++Measures parse time for SQL text containing many unique, unmatched ++opening delimiters for the two lexer constructs that used a lazy dot-all ++regex (`[\\s\\S]*?`) terminated by a backreference or a literal closing ++sequence: ++ ++- Dollar-quoted literals, e.g. `$a0$x $a1$x ... $aN$x` (backreference). ++- Multiline comments, e.g. `/* unique0 ... /* unique1 ...` (literal `*/`). ++ ++When no closing delimiter is present, a lazy dot-all quantifier applied at ++every text position must scan to the end of the remaining input for every ++opener, which is O(n^2) total work as the number of openers grows. ++ ++This benchmark does not assert a pass/fail threshold, since absolute timings ++and scaling ratios depend on the host machine. It exists to make the ++runtime characteristics of these code paths observable and to let it be ++re-run (e.g. after a fix) to confirm that scaling has improved. ++ ++Run with: python benchmarks/bench_dollar_quote_redos.py ++""" ++ ++import signal ++import time ++ ++import sqlparse ++from sqlparse.engine import grouping ++ ++# Disable the grouping-stage DoS guards. They fire only after lexing ++# completes and do not bound regex CPU time, so they would otherwise mask ++# the lexer's true (unbounded) timing behind a SQLParseError at larger n. ++grouping.MAX_GROUPING_DEPTH = None ++grouping.MAX_GROUPING_TOKENS = None ++ ++ ++def _alarm_handler(signum, frame): ++ raise TimeoutError() ++ ++ ++signal.signal(signal.SIGALRM, _alarm_handler) ++ ++ ++def measure(label, sql, fn): ++ signal.alarm(30) ++ t0 = time.perf_counter() ++ status = 'OK' ++ try: ++ fn(sql) ++ except sqlparse.exceptions.SQLParseError: ++ status = 'CAP' ++ except TimeoutError: ++ status = 'TIMEOUT' ++ finally: ++ signal.alarm(0) ++ dt = (time.perf_counter() - t0) * 1000 ++ print(f' {status:8} {dt:8.1f} ms {label} ({len(sql)} B)') ++ return dt ++ ++ ++def make_dollar_quote_payload(n): ++ # N unique, never-closed dollar-quote openers. Each is unique so the ++ # backreference regex cannot short-circuit on an earlier match. ++ return ' '.join(f'$a{i}$x' for i in range(n)) ++ ++ ++def make_comment_payload(n): ++ # N unique, never-closed multiline comment openers. No '*/' appears ++ # anywhere, so the closing literal can never short-circuit the scan. ++ return ' '.join(f'/* unique{i} comment never closed' for i in range(n)) ++ ++ ++def run_scaling(label, make_payload, sizes=(250, 500, 1000, 2000, 4000, 8000)): ++ print(f'{label}:') ++ timings = {} ++ for n in sizes: ++ sql = make_payload(n) ++ timings[n] = measure(f'{label} n={n}', sql, sqlparse.parse) ++ ++ print() ++ print('Scaling ratios (O(n^2) implies ~4x time per 2x input):') ++ for prev, curr in zip(sizes, sizes[1:]): ++ if timings[prev] > 0: ++ ratio = timings[curr] / timings[prev] ++ print(f' n={prev} -> n={curr} (input x{curr / prev:.1f}): ' ++ f'time ratio = {ratio:.2f}x') ++ print() ++ ++ ++run_scaling('Unmatched dollar-quote openers', make_dollar_quote_payload) ++run_scaling('Unclosed multiline comments', make_comment_payload) +diff --git a/sqlparse/keywords.py b/sqlparse/keywords.py +index 874431f..243f389 100644 +--- a/sqlparse/keywords.py ++++ b/sqlparse/keywords.py +@@ -5,7 +5,10 @@ + # This module is part of python-sqlparse and is released under + # the BSD License: https://opensource.org/licenses/BSD-3-Clause + ++import re ++ + from sqlparse import tokens ++from sqlparse.utils import _DelimiterOccurrence, resolve_paired_delimiters + + # object() only supports "is" and is useful as a marker + # use this marker to specify that the given regex in SQL_REGEX +@@ -13,12 +16,64 @@ from sqlparse import tokens + PROCESS_AS_KEYWORD = object() + + ++# Dollar-quoted literals (`$tag$...$tag$`) and multiline comments ++# (`/*...*/`, `/*+...*/`) used to be matched with per-position regexes ++# using a lazy dot-all quantifier (`[\s\S]*?`) terminated by a ++# backreference or a literal delimiter. Applied at every text position by ++# the lexer loop below, that shape is O(n^2) on adversarial input with ++# many unclosed openers, since each failed attempt re-scans to the end of ++# the remaining text (GHSA-prg7-hcfm-mfcr). They are resolved instead in ++# a single linear pass by find_delimited_spans(). ++_DOLLAR_QUOTE_DELIM = re.compile(r'\$(?:[_A-ZÀ-Ü]\w*)?\$', re.IGNORECASE | re.UNICODE) ++_DOLLAR_QUOTE_OPENER_OK = re.compile(r'(?