chore(deps): update dependency sqlparse to v0.6.0 [security] - #2772
Open
renovate[bot] wants to merge 1 commit into
Open
chore(deps): update dependency sqlparse to v0.6.0 [security]#2772renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
0.5.5→0.6.0sqlparse: Generated Python and PHP snippets allow SQL string breakout through unescaped backslashes
CVE-2026-59894 / GHSA-3496-9g83-7v6x / PYSEC-2026-3696
More information
Details
Summary
The documented Python and PHP output modes generate source-code snippets from caller-supplied SQL. Their output filters escape quote characters without first escaping existing backslashes. Crafted SQL can therefore neutralize the generated quote escape, terminate the intended language string, and place attacker-controlled code into the generated snippet. If a downstream consumer executes or imports that generated source, the injected code runs in the consumer's environment.
Details
The Python output filter places SQL in a single-quoted string and replaces each single quote with an escaped quote. The PHP output filter performs the equivalent operation for a double-quoted string. Neither transformation escapes pre-existing backslashes before escaping quotes. A backslash supplied immediately before a quote causes the generated backslash to be escaped instead of the quote, allowing the quote to close the string.
The affected modes are exposed through
sqlparse.format(..., output_format='python'),sqlparse.format(..., output_format='php'), and the correspondingsqlformat -loptions. Formatting produces the injected source but does not itself execute it; code execution occurs when a downstream workflow treats the generated snippet as Python or PHP code.Relevant code locations:
sqlparse/formatter.py:193— selection of the output-language filterssqlparse/filters/output.py:45— opening of the generated Python stringsqlparse/filters/output.py:65— incomplete Python quote escapingsqlparse/filters/output.py:91— opening of the generated PHP stringsqlparse/filters/output.py:114— incomplete PHP quote escapingPoC
A complete validated reproduction is attached as output_format_snippet_injection-poc.zip. The archive contains
reproduction/at its root, uses Git and Docker, and validates the Python output path by generating and executing a snippet containing a controlled marker-file write.Extract the archive beside this report, then run:
./reproduction/run.shObserved result:
The generated Python snippet placed the attacker-controlled
pathlib.Path(...).write_text(...)expression outside the intended SQL string. Executing the snippet wrote the expected proof marker, emittedEVOHUNT_OUTPUT_FORMAT_INJECTION_VERIFIED, and completed successfully.Verification method:
The verification helper calls
sqlparse.format(..., output_format='python'), executes the generated snippet, and fails unless the injected Python expression writes the exact proof marker file.Limitations:
No reproduction blocker was recorded. The attached harness directly verifies the Python output path; exploitation also requires a downstream consumer to execute or import the generated source.
Impact
This is source-code injection in the opt-in Python and PHP snippet-generation modes. An attacker who controls SQL converted by one of these modes can inject language code into the generated artifact. If that artifact is subsequently executed, the attacker can run code with the permissions and access of the downstream Python or PHP process.
The demonstrated end-to-end result is code execution through a generated Python snippet. Formatting the SQL alone does not execute the payload, and ordinary parsing, splitting, or formatting without these output modes is not shown to be affected.
Severity
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:H/SI:H/SA:LReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
sqlparse: Quadratic O(n²) DoS in group_comments
CVE-2026-71491 / GHSA-f2ff-p2ww-7p4p / PYSEC-2026-3697
More information
Details
Summary
A comment-only statement (
-- c\n*n) may cause a Denial of Service (DoS).Details
Location: sqlparse/engine/grouping.py:331-341 (
group_comments), invoked first ingroup()atgrouping.py:439. Reachable viasqlparse.parse()andsqlparse.format(sql, strip_comments=True).A statement made of many single-line comments (
'-- c\n'repeated) lexes in O(n) butgroup_commentsis O(n²):The
whileloop runs n times and eachtoken_next_by/token_not_matchingrescans the O(n) remaining tokens. When all tokens are comments/newlines nothing ever groups, yet the full scan is repeated per token.Two following factors increase the severity:
group_commentsruns first ingroup()(grouping.py:439), before the_group_matchingtoken-count guard (grouping.py:34-39). So the entire quadratic cost is paid even on oversized input.MAX_GROUPING_TOKENSdoes not provide protection on this vector.format(sql, strip_comments=True), used by query loggers, SQL firewalls, ORMs, and migration tools.PoC
Tested using Python 3.14:
Output:
Time 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.Impact
Denial of Service
Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
sqlparse: Inefficient Regex Handling of Dollar-Quoted SQL Literals Leads to ReDoS (Denial of Service)
CVE-2026-59893 / GHSA-prg7-hcfm-mfcr / PYSEC-2026-3698
More information
Details
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:33uses 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.pyas part ofSQL_REGEX: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):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 toparsestream(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_REGEXis tried at the current position.sqlparse/keywords.py:33— the backreference regex performs repeated delimiter searches.The
MAX_GROUPING_TOKENS = 10000limit insqlparse/engine/grouping.py:20fires 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:
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, commitc923da9).Using Docker (isolated reproduction):
Direct Python reproduction:
Expected output (super-linear scaling confirms ReDoS):
Attack input structure:
Each token
$ai$xresembles 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. Seereport_excerpt.mdfor 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_REGEXuse the same lazy dot-all shape, terminated by a literal delimiter instead of a backreference: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
Lexing-only timings on
0.5.6.dev0(commitf80af6a), isolating the regex work from grouping:Roughly 3.7x per doubling of the input, i.e. quadratic.
Note for reproduction:
"/*" * non 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(), orsqlparse.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.
Reproduction artifacts
Dockerfilepoc.pySeverity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
sqlparse: TokenList.init materializes O(subtree) value per group, causing CPU DoS before depth/token caps trigger
CVE-2026-54284 / GHSA-pwgv-4x5q-6m9f / PYSEC-2026-3699
More information
Details
Summary
sqlparseships hard limits (MAX_GROUPING_DEPTH=100,MAX_GROUPING_TOKENS=10000) intended to bound parsing work on attacker-supplied SQL, but the path that reaches those limits is itselfO(n*depth)per token-group construction. A ~1-2 KB SQL payload (e.g.SELECT (((((1))))) ...with 500-2000 nesting levels, or a 200-400-level nestedCASE WHENchain) drives the parser to spend multiple seconds of CPU before the depth cap raisesSQLParseError. Concretely: a 2 KB malicious payload consumes ~10 seconds of CPU per request on a single worker (~5000x CPU-to-input amplification), while a benign 1 KB SQL completes in ~3 ms.The root cause is
TokenList.__init__callingsuper().__init__(None, str(self)).TokenList.__str__flattens the entire subtree on every call, and grouping constructs a newTokenListfor every parenthesis / CASE / list group, so a tree of depthdwithntotal tokens performsO(n*d)flatten work just to materialize the cachedvaluefield, which is then never read for grouped nodes (they override__str__).This is a distinct quadratic from the input-size caps added in GHSA-2m57-hf25-phgg / GHSA-27jp-wm6q-gp25: those caps prevent unbounded work, but the time required to trigger the caps is itself superlinear in payload size.
Affected components
sqlparse0.5.5 (latest) and every prior version that shipsTokenList.__init__. The offending line has existed since the introduction of the cached-value invariant; the recent DoS-protection commit (da67ac1, 2025-12-08) added depth + token caps to_group_matching/_groupbut left the per-nodestr(self)materialization untouched.Vulnerable code (file:line)
sqlparse/sql.py#L162(release 0.5.5) /sqlparse/sql.py#L167(currentmaster):__str__recurses viaflatten()over the entire subtree belowself. EveryTokenListconstructed during grouping (everyParenthesis,Case,IdentifierList, etc.) runs this on its current children, which themselves recursively callflatten(). For grouping that builds a tree of depthdcontainingntokens, the construction cost isO(n * d).The grouping pipeline that triggers it lives at
sqlparse/engine/grouping.py#L80(group_parenthesis) andsqlparse/engine/grouping.py#L84(group_case). Both call_group_matchingwhich builds nestedParenthesis/CaseTokenListinstances bottom-up.Reachable / How input reaches the sink
sqlparse.parse(sql),sqlparse.format(sql, reindent=True), andsqlparse.split(sql)are the documented entry points and all flow intoengine/filter_stack.py:run→engine/grouping.py:group→group_parenthesis/group_case. There is no opt-in flag: the quadratic runs on default configuration whenever attacker-controlled SQL contains nested parentheses, nestedCASE WHEN, nested subqueries, or nestedARRAY[]literals.Real-world consumers that feed user input directly into these entry points include any SQL formatter web service (the
sqlformat.org-style class of tools), Django'sformat_debug_sql(django/db/backends/base/operations.py) used when a debug toolbar shows user-typed SQL, and downstream metadata libraries such assql-metadata(Parser(sql).columnstriggers the same O(n*d) path and reproduces the multi-second hang on the same inputs).Proof of concept
Minimal in-process reproduction (sqlparse 0.5.5, default settings, no caps overridden):
Output on the reporter's machine (Python 3.9, sqlparse 0.5.5, single core):
cProfileattribution (nested-paren n=500, 1008 B input, 3.1 s total):42 million
flatten()calls for a 1 KB input. The cap raises at depth 100, butTokenList.__init__ranstr(self)once per group construction and each call walked the partial subtree.End-to-end reproduction (against running consumer)
victim_app.py(a 50-line Flask formatter, the canonical sqlparse consumer pattern):Driver run (Python 3.9, sqlparse 0.5.5,
threaded=Falseso one worker per request):A 2 KB payload (
nested-paren n=1000) pins one worker for 10 seconds at 100% CPU. Withgunicorn -w Ndeploying the same app,Nconcurrent malicious requests exhaust every worker and bring the service down. The capSQLParseErrorexception is delivered to the caller, but only after the CPU work is already burnt.Impact
Nparallel requests, exhausts the worker pool.sql-metadata.Parser(sql).columnscallssqlparse.parseinternally and inherits the exact same hang (nested-paren n=1000→ 11.3 s).Suggested fix
Replace the eager
str(self)materialization with a single-pass concatenation of children's already-cachedvaluefields. TheToken.valueinvariantvalue == str(self) at constructionis preserved (children'svalueis itself built the same way bottom-up), but the per-node cost drops fromO(subtree)toO(len(self.tokens)):Measured against the 0.5.5 source tree with the patch applied locally and the full existing test-suite running (479 passed, 2 xfailed, 1 xpassed; the same baseline as unpatched
0d24023):End-to-end Flask
victim_appre-run against the patched library:The IN-tuple
format()vector observed atn=1000(3.8 s for ~10 KB input) is a separate quadratic in thereindentfilter (filters/reindent.py:_get_offset→_flatten_up_to_token) and is not covered by this advisory; please consider it as a follow-up if the maintainer would like a separate report.Fix PR
A fix PR against the temp private fork, mirroring the diff above with a regression test (
test_nested_paren_within_cap_under_50ms), is attached and linked from this advisory.Credit
Reported by tonghuaroot.
Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
sqlparse: Generated Python and PHP snippets allow SQL string breakout through unescaped backslashes
CVE-2026-59894 / GHSA-3496-9g83-7v6x / PYSEC-2026-3696
More information
Details
Summary
The documented Python and PHP output modes generate source-code snippets from caller-supplied SQL. Their output filters escape quote characters without first escaping existing backslashes. Crafted SQL can therefore neutralize the generated quote escape, terminate the intended language string, and place attacker-controlled code into the generated snippet. If a downstream consumer executes or imports that generated source, the injected code runs in the consumer's environment.
Details
The Python output filter places SQL in a single-quoted string and replaces each single quote with an escaped quote. The PHP output filter performs the equivalent operation for a double-quoted string. Neither transformation escapes pre-existing backslashes before escaping quotes. A backslash supplied immediately before a quote causes the generated backslash to be escaped instead of the quote, allowing the quote to close the string.
The affected modes are exposed through
sqlparse.format(..., output_format='python'),sqlparse.format(..., output_format='php'), and the correspondingsqlformat -loptions. Formatting produces the injected source but does not itself execute it; code execution occurs when a downstream workflow treats the generated snippet as Python or PHP code.Relevant code locations:
sqlparse/formatter.py:193— selection of the output-language filterssqlparse/filters/output.py:45— opening of the generated Python stringsqlparse/filters/output.py:65— incomplete Python quote escapingsqlparse/filters/output.py:91— opening of the generated PHP stringsqlparse/filters/output.py:114— incomplete PHP quote escapingPoC
A complete validated reproduction is attached as output_format_snippet_injection-poc.zip. The archive contains
reproduction/at its root, uses Git and Docker, and validates the Python output path by generating and executing a snippet containing a controlled marker-file write.Extract the archive beside this report, then run:
./reproduction/run.shObserved result:
The generated Python snippet placed the attacker-controlled
pathlib.Path(...).write_text(...)expression outside the intended SQL string. Executing the snippet wrote the expected proof marker, emittedEVOHUNT_OUTPUT_FORMAT_INJECTION_VERIFIED, and completed successfully.Verification method:
The verification helper calls
sqlparse.format(..., output_format='python'), executes the generated snippet, and fails unless the injected Python expression writes the exact proof marker file.Limitations:
No reproduction blocker was recorded. The attached harness directly verifies the Python output path; exploitation also requires a downstream consumer to execute or import the generated source.
Impact
This is source-code injection in the opt-in Python and PHP snippet-generation modes. An attacker who controls SQL converted by one of these modes can inject language code into the generated artifact. If that artifact is subsequently executed, the attacker can run code with the permissions and access of the downstream Python or PHP process.
The demonstrated end-to-end result is code execution through a generated Python snippet. Formatting the SQL alone does not execute the payload, and ordinary parsing, splitting, or formatting without these output modes is not shown to be affected.
Severity
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:H/SI:H/SA:LReferences
This data is provided by OSV and the PyPI Advisory Database (CC-BY 4.0).
sqlparse: Quadratic O(n²) DoS in group_comments
CVE-2026-71491 / GHSA-f2ff-p2ww-7p4p / PYSEC-2026-3697
More information
Details
Summary
A comment-only statement (
-- c\n*n) may cause a Denial of Service (DoS).Details
Location: sqlparse/engine/grouping.py:331-341 (
group_comments), invoked first ingroup()atgrouping.py:439. Reachable viasqlparse.parse()andsqlparse.format(sql, strip_comments=True).A statement made of many single-line comments (
'-- c\n'repeated) lexes in O(n) butgroup_commentsis O(n²):The
whileloop runs n times and eachtoken_next_by/token_not_matchingrescans the O(n) remaining tokens. When all tokens are comments/newlines nothing ever groups, yet the full scan is repeated per token.Two following factors increase the severity:
group_commentsruns first ingroup()(grouping.py:439), before the_group_matchingtoken-count guard (grouping.py:34-39). So the entire quadratic cost is paid even on oversized input.MAX_GROUPING_TOKENSdoes not provide protection on this vector.format(sql, strip_comments=True), used by query loggers, SQL firewalls, ORMs, and migration tools.PoC
Tested using Python 3.14:
Output:
Time 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.Impact
Denial of Service
Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by OSV and the PyPI Advisory Database (CC-BY 4.0).
sqlparse: Inefficient Regex Handling of Dollar-Quoted SQL Literals Leads to ReDoS (Denial of Service)
CVE-2026-59893 / GHSA-prg7-hcfm-mfcr / PYSEC-2026-3698
More information
Details
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:33uses 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.pyas part ofSQL_REGEX: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):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 toparsestream(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_REGEXis tried at the current position.sqlparse/keywords.py:33— the backreference regex performs repeated delimiter searches.The
MAX_GROUPING_TOKENS = 10000limit insqlparse/engine/grouping.py:20fires 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:
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, commitc923da9).Using Docker (isolated reproduction):
Direct Python reproduction:
Expected output (super-linear scaling confirms ReDoS):
Attack input structure:
Each token
$ai$xresembles 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. Seereport_excerpt.mdfor 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_REGEXuse the same lazy dot-all shape, terminated by a literal delimiter instead of a backreference: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
Lexing-only timings on
0.5.6.dev0(commitf80af6a), isolating the regex work from grouping:Roughly 3.7x per doubling of the input, i.e. quadratic.
Note for reproduction:
"/*" * non 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(), orsqlparse.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.
Reproduction artifacts
Dockerfilepoc.pySeverity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by OSV and the PyPI Advisory Database (CC-BY 4.0).
sqlparse: TokenList.init materializes O(subtree) value per group, causing CPU DoS before depth/token caps trigger
CVE-2026-54284 / GHSA-pwgv-4x5q-6m9f / PYSEC-2026-3699
More information
Details
Summary
sqlparseships hard limits (MAX_GROUPING_DEPTH=100,MAX_GROUPING_TOKENS=10000) intended to bound parsing work on attacker-supplied SQL, but the path that reaches those limits is itselfO(n*depth)per token-group construction. A ~1-2 KB SQL payload (e.g.SELECT (((((1))))) ...with 500-2000 nesting levels, or a 200-400-level nestedCASE WHENchain) drives the parser to spend multiple seconds of CPU before the depth cap raisesSQLParseError. Concretely: a 2 KB malicious payload consumes ~10 seconds of CPU per request on a single worker (~5000x CPU-to-input amplification), while a benign 1 KB SQL completes in ~3 ms.The root cause is
TokenList.__init__callingsuper().__init__(None, str(self)).TokenList.__str__flattens the entire subtree on every call, and grouping constructs a newTokenListfor every parenthesis / CASE / list group, so a