{"id":"CVE-2026-78683","aliases":["DEBIAN-CVE-2026-78683","EUVD-2026-65229","GHSA-gx65-c5hj-vpv5","GHSA-rhp5-r9x4-f5g2","MGASA-2026-0340","PYSEC-2026-3734"],"status":"published","title":"NLTK before 3.10.0 Remote Code Execution via Unsafe Pickle Deserialization","description":"NLTK before 3.10.0 (affected versions \u003c=3.9.4) contains an unsafe pickle deserialization vulnerability in the TransitionParser.parse() method (nltk/parse/transitionparser.py). The method calls pickle_load() with the default restricted=False, routing deserialization through WarningUnpickler, which does not override find_class() and therefore permits arbitrary class resolution. When an application loads an attacker-crafted model file, embedded pickle gadget chains execute arbitrary Python code with the privileges of the user running the application. NLTK provides a RestrictedUnpickler for safe deserialization, but it is not used by production code paths. Fixed in 3.10.0.","severity":{"version":"4.0","score":9.4,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H","level":"critical","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.0051,"percentile":0.43878,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-502"],"published":"2026-08-25T01:30:39Z","modified":"2026-10-28T02:30:34Z","fixes":[{"ecosystem":"PyPI","package":"nltk","fixed_in":["3.10.0"],"sources":["ghsa","osv"]},{"ecosystem":"Debian:14","package":"nltk","fixed_in":["3.10.0-1"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"nltk","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"3.9.4"},{"fixed":"3.10.0"}]}]},{"source":"osv","ecosystem":"Debian:12","package":"nltk","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["3.10.0-1","3.10.1-1","3.10.3-1","3.8-1","3.8.1-1","3.9.1-1","3.9.1-2","3.9.2-1","3.9.3-1"]},{"source":"osv","ecosystem":"Debian:13","package":"nltk","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["3.10.0-1","3.10.1-1","3.10.3-1","3.9.1-2","3.9.2-1","3.9.3-1"]},{"source":"osv","ecosystem":"Debian:14","package":"nltk","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.10.0-1"}]}],"versions":["3.9.1-2","3.9.2-1","3.9.3-1"]},{"source":"osv","ecosystem":"PyPI","package":"nltk","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.10.0"}]}],"versions":["0.8","0.9","0.9.3","0.9.4","0.9.5","0.9.6","0.9.7","0.9.8","0.9.9","2.0.1","2.0.1rc1","2.0.1rc2-git","2.0.1rc3","2.0.1rc4","2.0.2","2.0.3","2.0.4","2.0.5","2.0b4","2.0b5","2.0b6","2.0b7","2.0b8","2.0b9","3.0.0","3.0.0b1","3.0.0b2","3.0.1","3.0.2","3.0.3","3.0.4","3.0.5","3.1","3.2","3.2.1","3.2.2","3.2.3","3.2.4","3.2.5","3.3","3.4","3.4.1","3.4.2","3.4.3","3.4.4","3.4.5","3.5","3.5b1","3.6","3.6.1","3.6.2","3.6.3","3.6.4","3.6.5","3.6.6","3.6.7","3.7","3.8","3.8.1","3.9","3.9.1","3.9.2","3.9.3","3.9.4","3.9b1"]},{"source":"osv","ecosystem":"Mageia:10","package":"python-nltk","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.10.3-1.mga10"}]}],"bundled":18},{"source":"osv","ecosystem":"Mageia:9","package":"python-nltk","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.10.3-1.mga9"}]}],"bundled":18},{"source":"osv","ecosystem":"PyPI","package":"nltk","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.10.0"}]}],"versions":["0.8","0.9","0.9.3","0.9.4","0.9.5","0.9.6","0.9.7","0.9.8","0.9.9","2.0.1","2.0.1rc1","2.0.1rc2-git","2.0.1rc3","2.0.1rc4","2.0.2","2.0.3","2.0.4","2.0.5","2.0b4","2.0b5","2.0b6","2.0b7","2.0b8","2.0b9","3.0.0","3.0.0b1","3.0.0b2","3.0.1","3.0.2","3.0.3","3.0.4","3.0.5","3.1","3.2","3.2.1","3.2.2","3.2.3","3.2.4","3.2.5","3.3","3.4","3.4.1","3.4.2","3.4.3","3.4.4","3.4.5","3.5","3.5b1","3.6","3.6.1","3.6.2","3.6.3","3.6.4","3.6.5","3.6.6","3.6.7","3.7","3.8","3.8.1","3.9","3.9.1","3.9.2","3.9.3","3.9.4","3.9b1"]}],"products":[{"vendor":"nltk","product":"nltk"}],"references":[{"url":"https://github.com/nltk/nltk/security/advisories/GHSA-rhp5-r9x4-f5g2","tags":["vendor-advisory","exploit","Exploit","Vendor Advisory","advisory","web","evidence"]},{"url":"https://www.vulncheck.com/advisories/nltk-before-remote-code-execution-via-unsafe-pickle-deserialization","tags":["third-party-advisory","Third Party Advisory","advisory","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-78683","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-gx65-c5hj-vpv5"},{"url":"https://github.com/nltk/nltk/pull/3631","tags":["web"]},{"url":"https://github.com/nltk/nltk/commit/f26b3753038d937b68145daf15e9636f8451053c","tags":["web"]},{"url":"https://github.com/nltk/nltk/releases/tag/v3.10.0","tags":["web"]},{"url":"https://github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3734.yaml","tags":["web"]},{"url":"https://github.com/advisories/GHSA-rhp5-r9x4-f5g2"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/78xxx/CVE-2026-78683.json","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-78683","tags":["advisory"]},{"url":"https://github.com/nltk/nltk","tags":["package"]},{"url":"https://advisories.mageia.org/MGASA-2026-0340.html","tags":["advisory"]},{"url":"https://bugs.mageia.org/show_bug.cgi?id=36186","tags":["report"]},{"url":"https://github.com/nltk/nltk/releases#release-v3.10.0","tags":["web"]},{"url":"https://github.com/nltk/nltk/releases#release-v3.10.2","tags":["web"]},{"url":"https://github.com/nltk/nltk/releases#release-v3.10.3","tags":["web"]}],"sources":{"cve":[{"id":"CVE-2026-78683","title":"NLTK before 3.10.0 Remote Code Execution via Unsafe Pickle Deserialization","description":"NLTK before 3.10.0 (affected versions \u003c=3.9.4) contains an unsafe pickle deserialization vulnerability in the TransitionParser.parse() method (nltk/parse/transitionparser.py). The method calls pickle_load() with the default restricted=False, routing deserialization through WarningUnpickler, which does not override find_class() and therefore permits arbitrary class resolution. When an application loads an attacker-crafted model file, embedded pickle gadget chains execute arbitrary Python code with the privileges of the user running the application. NLTK provides a RestrictedUnpickler for safe deserialization, but it is not used by production code paths. Fixed in 3.10.0.","status":"published","published":"2026-08-25T01:30:39.451Z","modified":"2026-08-25T15:19:32.286Z","cvss":[{"version":"4.0","score":9.4,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H","level":"critical","assessor":"cna"},{"version":"3.1","score":9.6,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H","level":"critical","assessor":"cna"}],"cwes":["CWE-502"],"updated_at":"2026-11-02T13:34:59.78118Z"}],"euvd":[{"id":"EUVD-2026-65229","description":"NLTK before 3.10.0 (affected versions \u003c=3.9.4) contains an unsafe pickle deserialization vulnerability in the TransitionParser.parse() method (nltk/parse/transitionparser.py). The method calls pickle_load() with the default restricted=False, routing deserialization through WarningUnpickler, which does not override find_class() and therefore permits arbitrary class resolution. When an application loads an attacker-crafted model file, embedded pickle gadget chains execute arbitrary Python code with the privileges of the user running the application. NLTK provides a RestrictedUnpickler for safe deserialization, but it is not used by production code paths. Fixed in 3.10.0.","status":"published","published":"2026-08-25T01:30:39Z","modified":"2026-08-25T15:19:32Z","cvss":[{"version":"4.0","score":9.4,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H","level":"critical"}],"updated_at":"2026-11-02T18:01:32.513581Z"}],"ghsa":[{"id":"GHSA-gx65-c5hj-vpv5","title":"Duplicate Advisory: [CWE-502] Unsafe Pickle Deserialization in TransitionParser Allows Remote Code Execution","description":"## Duplicate Advisory\n\nThis advisory has been withdrawn because it is a duplicate of GHSA-rhp5-r9x4-f5g2. This link is maintained to preserve external references.\n\n## Original Description\nNLTK before 3.10.0 (affected versions \u003c=3.9.4) contains an unsafe pickle deserialization vulnerability in the TransitionParser.parse() method (nltk/parse/transitionparser.py). The method calls pickle_load() with the default restricted=False, routing deserialization through WarningUnpickler, which does not override find_class() and therefore permits arbitrary class resolution. When an application loads an attacker-crafted model file, embedded pickle gadget chains execute arbitrary Python code with the privileges of the user running the application. NLTK provides a RestrictedUnpickler for safe deserialization, but it is not used by production code paths. Fixed in 3.10.0.","status":"withdrawn","published":"2026-08-25T03:32:11Z","modified":"2026-09-02T14:40:51Z","cvss":[{"version":"3.1","score":9.6,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H","level":"critical"},{"version":"4.0","score":9.4,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"critical"}],"cwes":["CWE-502"],"updated_at":"2026-11-02T14:41:10.121131Z"},{"id":"GHSA-rhp5-r9x4-f5g2","title":"NLTK: Unsafe Pickle Deserialization in TransitionParser Allows Remote Code Execution","description":"## Summary\n\nThe NLTK library's `TransitionParser.parse()` method deserializes model files using `pickle_load()` with the default `restricted=False` parameter, allowing arbitrary Python code execution when loading a malicious model file. The library provides a `RestrictedUnpickler` class for safe deserialization, but it is never used by production code paths, leaving the vulnerability unpatched.\n\n## Root Cause\n\n**File:** `nltk/parse/transitionparser.py` (lines 542-557)\n\nThe `parse()` method calls `pickle_load(f)` without `restricted=True`, routing through `WarningUnpickler` which inherits from `pickle.Unpickler` and does NOT override `find_class()`. This allows arbitrary class/function resolution during unpickling, enabling RCE via standard pickle gadgets (e.g., `os.system`, `subprocess.Popen`).\n\n**Vulnerability chain in `nltk/picklesec.py`:**\n\n```python\ndef pickle_load(file, *, context=None, restricted=False):\n    if restricted:\n        return RestrictedUnpickler(file).load()  # Safe: blocks all globals\n    return WarningUnpickler(file, context=context).load()  # VULNERABLE PATH\n```\n\n`WarningUnpickler` only emits a warning but does NOT block unsafe class loading — it calls `super().load()` which is standard `pickle.Unpickler.load()`.\n\n**Why this is not by design:**\n- NLTK intentionally created `RestrictedUnpickler` to block unsafe deserialization\n- The `restricted=True` parameter exists in the API but is **never used** by any production code path\n- All call sites use the default `restricted=False`: `transitionparser.py:557`, `parse/chartparser_app.py:816`, `parse/chartparser_app.py:2273`, `parse/chartparser_app.py:2311`\n\n## Attack Surface\n\n**Entry point:** `TransitionParser().parse(depgraphs, modelFile)` receives a filesystem path with no validation.\n\n**Exploitation path:**\n1. Attacker places a malicious pickle file at a known or attacker-controlled location\n2. Victim calls `parser.parse(sentences, \"/path/to/malicious_model.pkl\")`\n3. `pickle_load()` deserializes the file with `restricted=False` (default)\n4. Standard pickle gadget chain executes arbitrary Python code with victim's privileges\n\n**Impact:** Remote code execution with the privileges of the user running the NLTK-dependent application. Affects researchers, data scientists, and automated ML pipelines using NLTK for parsing tasks.\n\n## Steps to Reproduce\n\n### Environment\n- NLTK version: 3.8.1+ (all versions with `transitionparser.py`)\n- Python 3.6+\n- No special dependencies required\n\n### Reproduction\n\n1. Create a malicious pickle file that uses `__reduce__` to execute a system command during deserialization.\n\n2. Call `TransitionParser().parse([], '/path/to/malicious_model.pkl')`.\n\n3. The `pickle_load(f)` call at `transitionparser.py:557` uses `restricted=False` by default, routing through `WarningUnpickler`, which does not override `find_class()` and permits full class resolution — executing the embedded gadget.\n\n4. Arbitrary code executes with the victim's privileges.\n\n### Proof That the Fix Works\n\nChanging line 557 in `transitionparser.py` from:\n```python\nmodel = pickle_load(f)\n```\nto:\n```python\nmodel = pickle_load(f, restricted=True)\n```\ncauses `RestrictedUnpickler` to raise an `UnpicklingError` and block execution, confirming the safe path prevents the attack.\n\n### Working PoC\n\n```python\nimport pickle\nimport os\nfrom nltk.parse.transitionparser import TransitionParser\n\n# Create malicious pickle with RCE payload\nclass Exploit:\n    def __reduce__(self):\n        return (os.system, ('touch /tmp/nltk_poc_triggered',))\n\nwith open('/tmp/malicious_model.pkl', 'wb') as f:\n    pickle.dump(Exploit(), f)\n\n# Trigger the vulnerable code path (requires algorithm argument in ≤ 3.9.4)\nparser = TransitionParser('arc-standard')      # or 'arc-eager'\nparser.parse([], '/tmp/malicious_model.pkl')   # loads and unpickles unsafely\n\n# Exploit succeeds: file /tmp/nltk_poc_triggered is created\n```\n\nOn NLTK ≥ 3.10.0 (patched), the same code fails with:\n\n```\n_pickle.UnpicklingError: global 'posix.system' is not in the pickle allowlist\n```\n\nThis proves the vulnerability exists in versions ≤ 3.9.4 and is fixed in 3.10.0+.\n\n## Recommended Fix\n\nChange all call sites to use `restricted=True`:\n\n| File | Line | Before | After |\n|------|------|--------|-------|\n| `nltk/parse/transitionparser.py` | 557 | `pickle_load(f)` | `pickle_load(f, restricted=True)` |\n| `nltk/parse/chartparser_app.py` | 816 | `pickle_load(model_data_file)` | `pickle_load(model_data_file, restricted=True)` |\n| `nltk/parse/chartparser_app.py` | 2273 | `pickle_load(file)` | `pickle_load(file, restricted=True)` |\n| `nltk/parse/chartparser_app.py` | 2311 | `pickle_load(fp)` | `pickle_load(fp, restricted=True)` |\n\n**Note:** This fix may affect loading older sklearn models. A more robust approach would implement a module allowlist in `RestrictedUnpickler.find_class()`.","status":"published","published":"2026-09-08T16:41:11Z","modified":"2026-09-08T16:41:13Z","cvss":[{"version":"4.0","score":9.4,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H","level":"critical"}],"cwes":["CWE-502"],"updated_at":"2026-11-02T14:41:13.477974Z"}],"nvd":[{"id":"CVE-2026-78683","description":"NLTK before 3.10.0 (affected versions \u003c=3.9.4) contains an unsafe pickle deserialization vulnerability in the TransitionParser.parse() method (nltk/parse/transitionparser.py). The method calls pickle_load() with the default restricted=False, routing deserialization through WarningUnpickler, which does not override find_class() and therefore permits arbitrary class resolution. When an application loads an attacker-crafted model file, embedded pickle gadget chains execute arbitrary Python code with the privileges of the user running the application. NLTK provides a RestrictedUnpickler for safe deserialization, but it is not used by production code paths. Fixed in 3.10.0.","status":"published","published":"2026-08-25T02:16:53.033Z","modified":"2026-10-12T17:10:00.163Z","cvss":[{"version":"4.0","score":9.4,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"critical","assessor":"secondary"},{"version":"3.1","score":9.6,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H","level":"critical","assessor":"secondary"}],"cwes":["CWE-502"],"updated_at":"2026-11-02T14:12:41.018848Z"}],"osv":[{"id":"CVE-2026-78683","title":"NLTK before 3.10.0 Remote Code Execution via Unsafe Pickle Deserialization","description":"NLTK before 3.10.0 (affected versions \u003c=3.9.4) contains an unsafe pickle deserialization vulnerability in the TransitionParser.parse() method (nltk/parse/transitionparser.py). The method calls pickle_load() with the default restricted=False, routing deserialization through WarningUnpickler, which does not override find_class() and therefore permits arbitrary class resolution. When an application loads an attacker-crafted model file, embedded pickle gadget chains execute arbitrary Python code with the privileges of the user running the application. NLTK provides a RestrictedUnpickler for safe deserialization, but it is not used by production code paths. Fixed in 3.10.0.","status":"published","published":"2026-08-25T01:30:39.451Z","modified":"2026-10-28T02:30:34.613233307Z","cvss":[{"version":"4.0","score":9.4,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H","level":"critical"}],"cwes":["CWE-502"],"updated_at":"2026-11-02T15:45:17.44708Z"},{"id":"DEBIAN-CVE-2026-78683","description":"NLTK before 3.10.0 (affected versions \u003c=3.9.4) contains an unsafe pickle deserialization vulnerability in the TransitionParser.parse() method (nltk/parse/transitionparser.py). The method calls pickle_load() with the default restricted=False, routing deserialization through WarningUnpickler, which does not override find_class() and therefore permits arbitrary class resolution. When an application loads an attacker-crafted model file, embedded pickle gadget chains execute arbitrary Python code with the privileges of the user running the application. NLTK provides a RestrictedUnpickler for safe deserialization, but it is not used by production code paths. Fixed in 3.10.0.","status":"published","published":"2026-08-25T02:16:53.033Z","modified":"2026-10-13T09:47:30.247671145Z","cvss":[{"version":"4.0","score":9.4,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"critical"}],"updated_at":"2026-11-02T15:46:03.502256Z"},{"id":"GHSA-gx65-c5hj-vpv5","title":"Duplicate Advisory: [CWE-502] Unsafe Pickle Deserialization in TransitionParser Allows Remote Code Execution","description":"## Duplicate Advisory\n\nThis advisory has been withdrawn because it is a duplicate of GHSA-rhp5-r9x4-f5g2. This link is maintained to preserve external references.\n\n## Original Description\nNLTK before 3.10.0 (affected versions \u003c=3.9.4) contains an unsafe pickle deserialization vulnerability in the TransitionParser.parse() method (nltk/parse/transitionparser.py). The method calls pickle_load() with the default restricted=False, routing deserialization through WarningUnpickler, which does not override find_class() and therefore permits arbitrary class resolution. When an application loads an attacker-crafted model file, embedded pickle gadget chains execute arbitrary Python code with the privileges of the user running the application. NLTK provides a RestrictedUnpickler for safe deserialization, but it is not used by production code paths. Fixed in 3.10.0.","status":"withdrawn","published":"2026-08-25T03:32:11Z","modified":"2026-09-02T15:00:09.005033152Z","cvss":[{"version":"3.1","score":9.6,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H","level":"critical"},{"version":"4.0","score":9.4,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"critical"}],"cwes":["CWE-502"],"updated_at":"2026-11-02T15:46:48.191583Z"},{"id":"GHSA-rhp5-r9x4-f5g2","title":"NLTK: Unsafe Pickle Deserialization in TransitionParser Allows Remote Code Execution","description":"## Summary\n\nThe NLTK library's `TransitionParser.parse()` method deserializes model files using `pickle_load()` with the default `restricted=False` parameter, allowing arbitrary Python code execution when loading a malicious model file. The library provides a `RestrictedUnpickler` class for safe deserialization, but it is never used by production code paths, leaving the vulnerability unpatched.\n\n## Root Cause\n\n**File:** `nltk/parse/transitionparser.py` (lines 542-557)\n\nThe `parse()` method calls `pickle_load(f)` without `restricted=True`, routing through `WarningUnpickler` which inherits from `pickle.Unpickler` and does NOT override `find_class()`. This allows arbitrary class/function resolution during unpickling, enabling RCE via standard pickle gadgets (e.g., `os.system`, `subprocess.Popen`).\n\n**Vulnerability chain in `nltk/picklesec.py`:**\n\n```python\ndef pickle_load(file, *, context=None, restricted=False):\n    if restricted:\n        return RestrictedUnpickler(file).load()  # Safe: blocks all globals\n    return WarningUnpickler(file, context=context).load()  # VULNERABLE PATH\n```\n\n`WarningUnpickler` only emits a warning but does NOT block unsafe class loading — it calls `super().load()` which is standard `pickle.Unpickler.load()`.\n\n**Why this is not by design:**\n- NLTK intentionally created `RestrictedUnpickler` to block unsafe deserialization\n- The `restricted=True` parameter exists in the API but is **never used** by any production code path\n- All call sites use the default `restricted=False`: `transitionparser.py:557`, `parse/chartparser_app.py:816`, `parse/chartparser_app.py:2273`, `parse/chartparser_app.py:2311`\n\n## Attack Surface\n\n**Entry point:** `TransitionParser().parse(depgraphs, modelFile)` receives a filesystem path with no validation.\n\n**Exploitation path:**\n1. Attacker places a malicious pickle file at a known or attacker-controlled location\n2. Victim calls `parser.parse(sentences, \"/path/to/malicious_model.pkl\")`\n3. `pickle_load()` deserializes the file with `restricted=False` (default)\n4. Standard pickle gadget chain executes arbitrary Python code with victim's privileges\n\n**Impact:** Remote code execution with the privileges of the user running the NLTK-dependent application. Affects researchers, data scientists, and automated ML pipelines using NLTK for parsing tasks.\n\n## Steps to Reproduce\n\n### Environment\n- NLTK version: 3.8.1+ (all versions with `transitionparser.py`)\n- Python 3.6+\n- No special dependencies required\n\n### Reproduction\n\n1. Create a malicious pickle file that uses `__reduce__` to execute a system command during deserialization.\n\n2. Call `TransitionParser().parse([], '/path/to/malicious_model.pkl')`.\n\n3. The `pickle_load(f)` call at `transitionparser.py:557` uses `restricted=False` by default, routing through `WarningUnpickler`, which does not override `find_class()` and permits full class resolution — executing the embedded gadget.\n\n4. Arbitrary code executes with the victim's privileges.\n\n### Proof That the Fix Works\n\nChanging line 557 in `transitionparser.py` from:\n```python\nmodel = pickle_load(f)\n```\nto:\n```python\nmodel = pickle_load(f, restricted=True)\n```\ncauses `RestrictedUnpickler` to raise an `UnpicklingError` and block execution, confirming the safe path prevents the attack.\n\n### Working PoC\n\n```python\nimport pickle\nimport os\nfrom nltk.parse.transitionparser import TransitionParser\n\n# Create malicious pickle with RCE payload\nclass Exploit:\n    def __reduce__(self):\n        return (os.system, ('touch /tmp/nltk_poc_triggered',))\n\nwith open('/tmp/malicious_model.pkl', 'wb') as f:\n    pickle.dump(Exploit(), f)\n\n# Trigger the vulnerable code path (requires algorithm argument in ≤ 3.9.4)\nparser = TransitionParser('arc-standard')      # or 'arc-eager'\nparser.parse([], '/tmp/malicious_model.pkl')   # loads and unpickles unsafely\n\n# Exploit succeeds: file /tmp/nltk_poc_triggered is created\n```\n\nOn NLTK ≥ 3.10.0 (patched), the same code fails with:\n\n```\n_pickle.UnpicklingError: global 'posix.system' is not in the pickle allowlist\n```\n\nThis proves the vulnerability exists in versions ≤ 3.9.4 and is fixed in 3.10.0+.\n\n## Recommended Fix\n\nChange all call sites to use `restricted=True`:\n\n| File | Line | Before | After |\n|------|------|--------|-------|\n| `nltk/parse/transitionparser.py` | 557 | `pickle_load(f)` | `pickle_load(f, restricted=True)` |\n| `nltk/parse/chartparser_app.py` | 816 | `pickle_load(model_data_file)` | `pickle_load(model_data_file, restricted=True)` |\n| `nltk/parse/chartparser_app.py` | 2273 | `pickle_load(file)` | `pickle_load(file, restricted=True)` |\n| `nltk/parse/chartparser_app.py` | 2311 | `pickle_load(fp)` | `pickle_load(fp, restricted=True)` |\n\n**Note:** This fix may affect loading older sklearn models. A more robust approach would implement a module allowlist in `RestrictedUnpickler.find_class()`.","status":"published","published":"2026-09-08T16:41:11Z","modified":"2026-09-08T16:45:04.193165862Z","cvss":[{"version":"4.0","score":9.4,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H","level":"critical"}],"cwes":["CWE-502"],"updated_at":"2026-11-02T15:46:55.877614Z"},{"id":"PYSEC-2026-3734","description":"NLTK before 3.10.0 (affected versions \u003c=3.9.4) contains an unsafe pickle deserialization vulnerability in the TransitionParser.parse() method (nltk/parse/transitionparser.py). The method calls pickle_load() with the default restricted=False, routing deserialization through WarningUnpickler, which does not override find_class() and therefore permits arbitrary class resolution. When an application loads an attacker-crafted model file, embedded pickle gadget chains execute arbitrary Python code with the privileges of the user running the application. NLTK provides a RestrictedUnpickler for safe deserialization, but it is not used by production code paths. Fixed in 3.10.0.","status":"published","published":"2026-08-25T02:16:53.033Z","modified":"2026-09-01T09:15:02.843391088Z","cvss":[{"version":"4.0","score":9.4,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"critical"}],"updated_at":"2026-11-02T15:52:57.255153Z"},{"id":"MGASA-2026-0340","title":"Updated python-nltk packages fix security vulnerabilities","description":"Updated packages fix security vulnerabilities. Please see the links.\n","status":"published","published":"2026-08-29T15:47:31Z","modified":"2026-08-29T16:00:02.787972151Z","updated_at":"2026-11-02T15:49:43.795591Z"}]}}
