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 <albrecht.andi@gmail.com>
+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 <ankur.tyagi85@gmail.com>
+---
+ 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'(?<![\w"$])', re.UNICODE)
++_COMMENT_HINT_OPEN = re.compile(r'/\*\+')
++_COMMENT_OPEN = re.compile(r'/\*(?!\+)')
++_COMMENT_CLOSE = re.compile(r'\*/')
++
++
++def find_delimited_spans(text):
++    """Locate dollar-quoted literals and multiline comments in `text`.
++
++    Returns a dict mapping each span's start offset to
++    (end offset, token type).
++    """
++    has_dollar = '$' in text
++    # A comment can only ever open on "/*"; without it "*/" alone can
++    # never pair with anything, so gating on "/*" alone is sufficient to
++    # skip all three comment-related regex passes below.
++    has_comment_open = '/*' in text
++    if not has_dollar and not has_comment_open:
++        return {}
++
++    occurrences = []
++    if has_dollar:
++        for m in _DOLLAR_QUOTE_DELIM.finditer(text):
++            tag = m.group()
++            can_open = _DOLLAR_QUOTE_OPENER_OK.match(text, m.start()) is not None
++            occurrences.append(
++                _DelimiterOccurrence(m.start(), m.end(), tag, can_open, True, tokens.Literal))
++    if has_comment_open:
++        for m in _COMMENT_HINT_OPEN.finditer(text):
++            occurrences.append(_DelimiterOccurrence(
++                m.start(), m.end(), 'C', True, False,
++                tokens.Comment.Multiline.Hint))
++        for m in _COMMENT_OPEN.finditer(text):
++            occurrences.append(_DelimiterOccurrence(
++                m.start(), m.end(), 'C', True, False,
++                tokens.Comment.Multiline))
++        for m in _COMMENT_CLOSE.finditer(text):
++            occurrences.append(
++                _DelimiterOccurrence(m.start(), m.end(), 'C', False, True, None))
++    occurrences.sort(key=lambda occ: occ.start)
++
++    spans = resolve_paired_delimiters(occurrences)
++    return {start: (end, ttype) for start, end, ttype in spans}
++
++
+ SQL_REGEX = [
+     (r'(--|# )\+.*?(\r\n|\r|\n|$)', tokens.Comment.Single.Hint),
+-    (r'/\*\+[\s\S]*?\*/', tokens.Comment.Multiline.Hint),
+ 
+     (r'(--|# ).*?(\r\n|\r|\n|$)', tokens.Comment.Single),
+-    (r'/\*[\s\S]*?\*/', tokens.Comment.Multiline),
+ 
+     (r'(\r\n|\r|\n)', tokens.Newline),
+     (r'\s+?', tokens.Whitespace),
+@@ -30,7 +85,6 @@ SQL_REGEX = [
+ 
+     (r"`(``|[^`])*`", tokens.Name),
+     (r"´(´´|[^´])*´", tokens.Name),
+-    (r'((?<![\w\"\$])\$(?:[_A-ZÀ-Ü]\w*)?\$)[\s\S]*?\1', tokens.Literal),
+ 
+     (r'\?', tokens.Name.Placeholder),
+     (r'%(\(\w+\))?s', tokens.Name.Placeholder),
+diff --git a/sqlparse/lexer.py b/sqlparse/lexer.py
+index 8f88d17..71fd261 100644
+--- a/sqlparse/lexer.py
++++ b/sqlparse/lexer.py
+@@ -134,8 +134,16 @@ class Lexer:
+             raise TypeError("Expected text or file-like object, got {!r}".
+                             format(type(text)))
+ 
++        delimited_spans = keywords.find_delimited_spans(text)
++
+         iterable = enumerate(text)
+         for pos, char in iterable:
++            if pos in delimited_spans:
++                end, ttype = delimited_spans[pos]
++                yield ttype, text[pos:end]
++                consume(iterable, end - pos - 1)
++                continue
++
+             for rexmatch, action in self._SQL_REGEX:
+                 m = rexmatch(text, pos)
+ 
+diff --git a/sqlparse/utils.py b/sqlparse/utils.py
+index 58c0245..ed83734 100644
+--- a/sqlparse/utils.py
++++ b/sqlparse/utils.py
+@@ -7,7 +7,7 @@
+ 
+ import itertools
+ import re
+-from collections import deque
++from collections import defaultdict, deque, namedtuple
+ from contextlib import contextmanager
+ 
+ # This regular expression replaces the home-cooked parser that was here before.
+@@ -110,6 +110,50 @@ def consume(iterator, n):
+     deque(itertools.islice(iterator, n), maxlen=0)
+ 
+ 
++_DelimiterOccurrence = namedtuple(
++    '_DelimiterOccurrence', 'start end tag can_open can_close payload')
++
++
++def resolve_paired_delimiters(occurrences):
++    """Pair delimiter occurrences (quote/comment open & close markers) in
++    one left-to-right pass, instead of re-scanning the remaining text for
++    every unmatched opener -- which is what a backreference- or literal-
++    terminated lazy-dot-all regex applied at every text position ends up
++    doing, and is O(n^2) on adversarial input (GHSA-prg7-hcfm-mfcr).
++
++    `occurrences` must be sorted by `start`. Each occurrence with
++    `can_open` is paired with the nearest later occurrence sharing its
++    `tag` that has `can_close`; anything in between (including other
++    openers) is left as literal content. Unpaired openers are dropped,
++    exactly as a regex that never finds its closing delimiter fails to
++    match at all.
++
++    Returns a list of (start, end, payload) for each resolved span.
++    """
++    occurrences = list(occurrences)
++    closers = defaultdict(deque)
++    for idx, occ in enumerate(occurrences):
++        if occ.can_close:
++            closers[occ.tag].append(idx)
++
++    spans = []
++    consumed_until = 0
++    for i, occ in enumerate(occurrences):
++        if occ.can_close:
++            queue = closers[occ.tag]
++            if queue and queue[0] == i:
++                queue.popleft()
++        if occ.start < consumed_until or not occ.can_open:
++            continue
++        queue = closers[occ.tag]
++        if queue:
++            close_idx = queue.popleft()
++            close_end = occurrences[close_idx].end
++            spans.append((occ.start, close_end, occ.payload))
++            consumed_until = close_end
++    return spans
++
++
+ @contextmanager
+ def offset(filter_, n=0):
+     filter_.offset += n
diff --git a/meta-python/recipes-devtools/python/python3-sqlparse_0.5.5.bb b/meta-python/recipes-devtools/python/python3-sqlparse_0.5.5.bb
index 021ccfa349..a1dc52db49 100644
--- a/meta-python/recipes-devtools/python/python3-sqlparse_0.5.5.bb
+++ b/meta-python/recipes-devtools/python/python3-sqlparse_0.5.5.bb
@@ -8,6 +8,7 @@ SRC_URI[sha256sum] = "e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa9
 
 SRC_URI += "file://CVE-2026-54284-1.patch \
             file://CVE-2026-54284-2.patch \
+            file://CVE-2026-59893.patch \
 "
 
 CVE_PRODUCT = "sqlparse"
