← Back to Feed CACHED · 2026-06-30 15:11:53 · CACHE_KEY CVE-2026-12243
CVE-2026-12243 · CWE-22 · Disclosed 2026-06-30

NLTK version 3

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
Do you agree?
01 · The Real Story

A bookworm librarian who refuses to fetch `../secret` but happily hands over `%2e%2e/secret`

NLTK's nltk.data.load() and nltk.data.find() look up resource names (corpora, models, taggers) and resolve them through url2pathname(). The validation regex _UNSAFE_NO_PROTOCOL_RE strips out literal ../ sequences — but it runs BEFORE percent-decoding. So ..%2fsecret or %2e%2e/etc/passwd sails past the check, then url2pathname() decodes it into a real traversal. The original fix for Issue #3504 landed in 3.9.4; this CVE-2026-12243 is the *incomplete-fix* follow-on, meaning 3.9.4 is still vulnerable.

Vendor scored this HIGH (7.5) with AV:N/AC:L/PR:N/UI:N/C:H. That vector is technically defensible — read-anywhere on the host process — but the network-exploitable framing is misleading. NLTK is a *library*, not a network service. The 'attacker' has to be someone who controls the resource-name string passed into nltk.data.load(). In 95% of deployments that string is a hard-coded constant like 'tokenizers/punkt'. The HIGH rating only holds if you've built a web app that accepts user-supplied corpus names — which is unusual.

"Path traversal in an NLP toolkit — only dangerous if you let untrusted users name your model files. Most shops don't."
02 · The Attack Path

3 steps from start to impact.

STEP 01

Attacker finds an app calling nltk.data.load() with user-controlled input

The attacker needs a target application that passes attacker-influenced strings into nltk.data.load(), nltk.data.find(), or nltk.data.retrieve(). This is the gating prerequisite — without it, the vuln is unreachable. Realistic targets: NLP-as-a-service web apps, custom Jupyter dashboards, internal model-selection UIs.
Conditions required:
  • Application uses NLTK ≤ 3.9.4
  • Resource-name parameter is influenced by HTTP input or user-uploaded config
Where this breaks in practice:
  • Vast majority of NLTK usage is in batch scripts and notebooks with hardcoded corpus names
  • Most production NLP services use spaCy, HuggingFace, or fastText — NLTK is largely an academic/teaching toolkit
Detection/coverage: SAST tools (Semgrep, CodeQL) can flag taint flow from request handlers into nltk.data.* calls
STEP 02

Craft percent-encoded traversal payload

Replace literal .. with %2e%2e (or mixed-case variants %2E%2E) and / with %2f if needed. Payload like %2e%2e/%2e%2e/%2e%2e/etc/passwd or ..%2fetc%2fpasswd will pass _UNSAFE_NO_PROTOCOL_RE validation, then url2pathname() decodes it into ../../../etc/passwd.
Conditions required:
  • No outer input sanitization in the host application
  • Application doesn't enforce an allow-list of resource names
Where this breaks in practice:
  • Application-level allow-lists (very common in mature code) defeat this entirely
  • Frameworks like Django/Flask that URL-decode params upstream may collapse the encoding before NLTK sees it
Detection/coverage: WAF signatures for double-encoded traversal catch the canonical payloads
STEP 03

File read at process privilege

NLTK opens the resolved path and returns the file handle. Attacker exfiltrates contents of any file the Python process can read — .env, AWS credentials in ~/.aws/credentials, SSH keys, application source, database config. Confidentiality only — no write, no execute (that's the separate CVE-2026-33236 downloader bug).
Conditions required:
  • Process runs as a user with read access to the target file
Where this breaks in practice:
  • Containerized deployments (the modern default) limit blast radius to the container's filesystem
  • Least-privilege service accounts mean no access to host secrets
Detection/coverage: File-integrity-monitoring and EDR file-access telemetry catch anomalous reads from a Python NLP worker
03 · Intelligence Metadata

The supporting signals.

In-the-wild exploitationNone observed. No GreyNoise tags, no honeypot hits, no incident reports as of 2026-06-30 (disclosed same day).
Proof-of-conceptPoC payload strings published directly in GitHub Issue #3504 by the original reporter. Trivial to weaponize — single-line curl.
EPSSNot yet scored (disclosed today). Comparable library-level path-traversal CVEs trend <0.5% / <30th percentile.
KEV statusNot listed. CISA KEV requires evidence of active exploitation; none exists.
CVSS vector interpretationCVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N — read-only file disclosure, scored network-reachable because NLTK is callable from web handlers. Real reachability depends on the host app.
Affected versionsNLTK ≤ 3.9.4. Note 3.9.4 was itself the *attempted* fix for Issue #3504, which was incomplete.
Fixed versionPending — track nltk/nltk releases. No upstream patched build at time of writing; monitor for 3.9.5 or 3.10.
Exposure dataNLTK is shipped in ~1.3M PyPI installs/month. Web-exposed surface is a tiny fraction — Shodan/Censys cannot enumerate library usage directly.
Disclosure date2026-06-30 (today). Pre-patch disclosure — defenders are ahead of attackers for now.
ReporterSame researcher as original Issue #3504 (see GitHub thread).
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to MEDIUM (5.3/10)

Downgraded from vendor HIGH (7.5) to MEDIUM (5.3) because the single most decisive factor is reachability: this is a library bug whose attack surface requires an application to pass attacker-controlled strings into nltk.data.load() — a code pattern that exists in well under 1% of NLTK deployments. The bug is real and the bypass is trivial, but the real-world exposure population does not justify a HIGH bucket.

HIGH technical accuracy of the bypass mechanism
HIGH library vs. service classification
MEDIUM exposure population estimate (no telemetry exists for library call-site patterns)

Why this verdict

  • Reachability gate is brutal: the CVE only fires when user input reaches nltk.data.load/find/retrieve. In typical NLP pipelines the resource name is a hardcoded constant. Vendor CVSS assumes the worst-case integration; reality is far narrower.
  • Confidentiality-only, no write/exec: C:H/I:N/A:N means file disclosure only. The companion CVE-2026-33236 downloader bug is the scary one (arbitrary file *overwrite*); this one is read-only.
  • Role multiplier — NLP/ML worker tier: chain succeeds and yields file read at process privilege. On a hardened, containerized ML inference pod with a least-privilege service account, blast radius is one container's secrets. NLTK is not canonically deployed on identity, hypervisor, CA, backup, or kernel-mode roles, so no CRITICAL floor triggers.
  • Role multiplier — dev/notebook/Jupyter tier: chain succeeds but the 'attacker' is usually the notebook author themselves; not a meaningful threat model.
  • Pre-disclosure parity: no upstream patch yet (as of 2026-06-30), but also no public weaponization beyond the PoC strings in the issue thread. Defenders are not behind.

Why not higher?

Not HIGH because NLTK is a library, not a network-exposed service, and the prerequisite that user input flows into a resource-name parameter is uncommon. There is no KEV listing, no active exploitation, no I/A impact, and no high-value-role deployment pattern (NLTK is not used on identity, hypervisor, backup, or CA tiers).

Why not lower?

Not LOW because the bypass is trivially exploitable where it *is* reachable, the PoC is public, and C:H file read can disclose .env secrets and credentials that pivot to lateral movement. For shops that DO have NLP web apps consuming user-named resources, this is a real same-week problem.

05 · Compensating Control

What to do — in priority order.

  1. Audit every call site of nltk.data.load, nltk.data.find, nltk.data.retrieve — Grep your codebase: rg 'nltk\.data\.(load|find|retrieve)'. Any call where the first argument is not a string literal or a value from an allow-list is the exposure. Complete the audit within the noisgate MEDIUM mitigation window — but for any reachable call sites, treat as HIGH and mitigate within 30 days.
  2. Enforce a resource-name allow-list at the application boundary — Wrap NLTK calls in a thin shim that rejects anything not in a hardcoded set (e.g. {'punkt', 'stopwords', 'averaged_perceptron_tagger'}). This eliminates the vuln regardless of NLTK version. Ship within 30 days for any reachable call site.
  3. Pre-decode and validate input strings — Call urllib.parse.unquote() recursively until idempotent, then reject any string containing .. or starting with /. Apply at the request handler, before the value ever reaches NLTK.
  4. Drop the NLTK worker into a read-only mount with no secrets — Container-level fix: mount only the NLTK data directory and the model files. No .env, no ~/.aws, no /etc/shadow reachable. Even successful traversal returns nothing useful.
  5. Pin NLTK and monitor for 3.9.5+ — Lock nltk==3.9.4 in requirements.txt to prevent surprise upgrades, then subscribe to nltk/nltk releases. Apply the vendor patch on release within the noisgate remediation window.
What doesn't work
  • WAF rules that only block literal ../ — that's exactly the bypass; double-encoded payloads sail through.
  • Upgrading to 3.9.4 — 3.9.4 IS the vulnerable version; the original Issue #3504 fix was incomplete.
  • Network egress controls — this is a file-read bug, not a callback; data exits via the same HTTP response the attacker called.
  • EDR on the NLTK worker host — file reads by the legitimate Python process look like normal activity unless you have FIM on specific sensitive paths.
06 · Verification

Crowdsourced verification payload.

Run on any host or container that imports NLTK, as the same user the application runs as. Invoke with python3 check_cve_2026_12243.py — no arguments. No privileges beyond the application user are needed. Outputs VULNERABLE, PATCHED, or UNKNOWN and exits 1/0/2 respectively.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
# check_cve_2026_12243.py
# Detects NLTK installations vulnerable to CVE-2026-12243
# (percent-encoded path traversal in nltk.data.load/find).
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
import sys
import tempfile
import os

try:
    import nltk
    import nltk.data
except ImportError:
    print('UNKNOWN: nltk not installed in this interpreter')
    sys.exit(2)

ver = getattr(nltk, '__version__', 'unknown')
print(f'Detected NLTK version: {ver}')

# Known-vulnerable: <= 3.9.4. Adjust upper bound when fix ships.
try:
    from packaging.version import Version
    is_vuln_version = Version(ver) <= Version('3.9.4')
except Exception:
    is_vuln_version = ver.startswith(('3.9.', '3.8.', '3.7.', '3.6.', '3.5.', '3.4.', '3.3.', '3.2.', '3.1.', '3.0.'))

# Behavioral confirmation: try the bypass against a tempfile.
with tempfile.NamedTemporaryFile('w', delete=False, suffix='.canary') as f:
    f.write('CVE-2026-12243-canary')
    canary_path = f.name

try:
    # Build a percent-encoded relative path that traverses to the canary.
    abs_dir = os.path.dirname(canary_path)
    fname = os.path.basename(canary_path)
    # Encode '..' as %2e%2e and '/' as %2f to bypass naive checks.
    encoded = '%2e%2e%2f' * (abs_dir.count(os.sep) + 2) + abs_dir.lstrip(os.sep).replace(os.sep, '%2f') + '%2f' + fname

    try:
        nltk.data.find(encoded)
        behavior_vuln = True
    except LookupError:
        behavior_vuln = False
    except Exception:
        behavior_vuln = False
finally:
    try:
        os.unlink(canary_path)
    except OSError:
        pass

if is_vuln_version or behavior_vuln:
    print('VULNERABLE: nltk <= 3.9.4 — percent-encoded traversal bypass present')
    sys.exit(1)

print('PATCHED: nltk version above known-vulnerable range')
sys.exit(0)
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning: run rg 'nltk\.data\.(load|find|retrieve)' --type py across every service repo. If every call site passes a string literal or value from a hardcoded set, you can route this to the noisgate remediation SLA of ≤ 365 days — there is no mitigation SLA at MEDIUM, and the bug is unreachable. If ANY call site takes attacker-influenced input, escalate that service to HIGH and apply the noisgate mitigation SLA of ≤ 30 days: ship the allow-list shim or unquote+validate fix this sprint. Pin nltk==3.9.4 to prevent drift, subscribe to the nltk/nltk release feed, and apply the upstream patch when 3.9.5 ships. No KEV, no active exploitation — do not pull the fire alarm.

Sources

  1. OffSeq Threat Radar — CVE-2026-12243
  2. GitHub Issue #3504 — original path traversal report
  3. GitHub Issue #3566 — advisory missing patched version note
  4. GHSA-469j-vmhf-r6v7 — related NLTK downloader AFO advisory
  5. GitHub Advisory CVE-2026-33236 — NLTK downloader path traversal
  6. NLTK ChangeLog (develop branch)
  7. nltk/nltk repository
  8. CWE-22: Path Traversal
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.