CVE-2026-79657 — Vulnerability in nltk — CVE Details
CRITICAL
NLTK: Allowlisted pickle loaders still permit code execution in current source
Published September 8, 2026Updated September 8, 2026Source: osv
Summary
### Summary
The current source tree still allows arbitrary code execution during supposedly safer allowlisted pickle loading. The allowlist trusts whole module namespaces instead of exact safe globals, so crafted pickles can invoke dangerous in-namespace callables through pickle REDUCE.
### Details
- **Vulnerability type:** Remote code execution via unsafe deserialization
- **Affected component:** `nltk.picklesec.allowlisted_pickle_load`, `nltk.tokenize.punkt.punkt_pickle_load`, `nltk.parse.transitionparser.TransitionParser.parse`
- **Affected versions:** Current source `v3.10.0-rc2`; published `3.9.4` was not the claim target for this bypass.
- **Patched versions:** Not yet patched
- **Root cause:** Module-prefix allowlists include dangerous callables such as `nltk.tokenize.repp.ReppTokenizer._execute` and `numpy.f2py.crackfortran.myeval`.
`punkt_pickle_load()` allowlists both `nltk.tokenize.punkt` and the whole `nltk.tokenize` namespace, which exposes `ReppTokenizer._execute()` and its `subprocess.Popen(...)` sink during unpickling. `TransitionParser.parse()` uses `allowlisted_pickle_load(..., allowed_modules=("numpy", "scipy", "sklearn"))`, which permits `numpy.f2py.crackfortran.myeval()` and its attacker-controlled `eval(...)` path. I confirmed both gadgets create marker files before the caller returns or later aborts on type misuse.
### PoC
**Preconditions**
- The application loads an attacker-controlled tokenizer or model artifact through these public loaders.
**Steps**
1. Create a pickle whose REDUCE callable is `ReppTokenizer._execute` and point its command to a harmless marker-file write.
2. Pass that payload to `punkt_pickle_load(BytesIO(payload))` and observe the marker file is created during unpickling.
3. Create a second pickle whose REDUCE callable is `numpy.f2py.crackfortran.myeval` and load it through `TransitionParser.parse()`.
4. Observe the second marker file is created before `TransitionParser.parse()` later fails on the returned object type.
**Minimal reproducible excerpt**
```text
{'punkt_marker': 'PUNKT_RCE', 'transitionparser_marker': 'TP_RCE'}
```
### Impact
Any caller that trusts these current allowlisted loaders can still execute attacker-controlled commands while loading model or tokenizer artifacts. This defeats the protection mechanism that replaced unrestricted pickle loading and creates a dangerous false sense of safety.
### Remediation
Replace broad module-prefix allowlists with exact `(module, qualname)` pairs for the few safe classes or functions genuinely required. Do not allow entire namespaces such as `nltk.tokenize` or `numpy`, and keep post-load type validation only as a secondary defense.
### Resources
- https://github.com/nltk/nltk/blob/v3.10.0-rc2/nltk/tokenize/punkt.py#L120-L134
- https://github.com/nltk/nltk/blob/v3.10.0-rc2/nltk/tokenize/repp.py#L111-L115
- https://github.com/nltk/nltk/blob/v3.10.0-rc2/nltk/parse/transitionparser.py#L26-L30
- https://github.com/nltk/nltk/blob/v3.10.0-rc2/nltk/parse/transitionparser.py#L565-L571
---
## Fix + attack demonstration (verified)
+ tightened callers
`find_class` now, before the allowlists:
1. **Rejects any dotted `name`** → closes 4489 with zero legit impact.
2. **Denies dangerous modules** (`os`, `subprocess`, `sys`, `builtins`, `numpy.f2py`, `nltk.tokenize.repp`, …) even under a broad `allowed_modules` — a defense-in-depth **backstop** so a future too-broad allowlist can't silently reopen RCE.
3. **`builtins` denied wholesale**; safe primitives (`int`, `str`, …) must be named exactly via `allowed_globals`.
Callers tightened: punkt drops the broad `nltk.tokenize` (keeps `nltk.tokenize.punkt` + exact `collections.defaultdict`/`builtins.int`); transitionparser keeps numpy/scipy/sklearn (array unpickling needs their submodules) with the new guards blocking the gadgets.
## Full pickle-sink audit
Every deserialization sink in the tree was reviewed: **no raw `pickle.load`** anywhere, and **no** joblib/numpy/torch/dill/yaml/marshal loaders. `data.load` + `wordnet_app` use `RestrictedUnpickler` (blocks all globals — safe); the remaining `pickle_load` sites (`chartparser_app`, `tbl/demo`) load user-selected or self-written files and keep their warning.
## Attack demonstration (captured; fork clone)
```
=== EXPLOITS blocked ===
4489 sklearn.os.system (dotted) -> BLOCKED
x99w numpy.f2py.crackfortran.myeval -> BLOCKED
x99w nltk.tokenize.repp._execute -> BLOCKED
backstop os.system (os allowlisted) -> BLOCKED
backstop builtins.eval (exact global)-> BLOCKED
=== LEGIT loads still work ===
punkt round-trip via punkt_pickle_load -> OK
builtins.int (safe primitive) -> OK
```
## Tests
`test_pickle_allowlist_security.py` — added 5 regressions (dotted traversal, both namespace gadgets, denied-module backstop, legit round-trip). Suite: 122 passed / 9 skipped (sklearn-dependent) across pickle/punkt/transition/tokenize. pre-commit (black/isort/ruff) clean.
Advisory Timeline
First published September 8, 2026 (34 days ago).
Last revised September 8, 2026 — 0 days after initial publication.
Remediation
Upgrade to the fixed version using your package manager.
pip
Update nltk to 3.10.3 or later
pip install "nltk>=3.10.3"
After upgrading, run your dependency scanner again to confirm the vulnerability is resolved.
NLTK: Allowlisted pickle loaders still permit code execution in current source This vulnerability has been assigned a severity rating of CRITICAL (CVSS score: 9.8/10).
How do I check if my project is affected by CVE-2026-79657?
CVE-2026-79657 affects nltk. Use GeekWala's free vulnerability scanner to check your dependencies against CVE-2026-79657 and 200,000+ other known vulnerabilities.
Severity & Exploitability
CVSS Score
9.8
Exploitation is straightforward and causes maximum impact. Patch immediately.