deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

NLTK 3.10 fixes percent-encoding bypass that enabled arbitrary file reads

A decode-after-check flaw in NLTK's resource loading let percent-encoded paths escape the data directory and read arbitrary files; version 3.10.0 re-validates decoded paths to close the gap.

NLTK 3.10 fixes percent-encoding bypass that enabled arbitrary file reads

NLTK, one of the most widely used natural language processing libraries in the Python ecosystem, has fixed a high-severity path traversal flaw that allowed arbitrary file reads. Tracked as CVE-2026-12243 and rated 7.5 High on CVSS 3.1, the vulnerability affects NLTK up to and including 3.9.4 and is patched in 3.10.0, according to a technical write-up published on dev.to.

How the bypass works

The bug lives in nltk.data.load() and nltk.data.find(), the two functions on the hot path whenever a corpus or model is loaded. Both turn a resource-name string into a filesystem path, and NLTK guards that conversion with a regex in nltk/data.py that rejects literal ../, a leading slash, backslashes and Windows drive letters.

The problem is what gets checked. The regex only ever runs against the raw, still URL-encoded string; the very next line calls the standard library's url2pathname(), which decodes percent sequences as a side effect. A resource name such as corpora/..%2f..%2f..%2fetc%2fpasswd therefore contains no literal ../ at check time, passes the filter, and only decodes into a real traversal after validation is done. The write-up describes this as a textbook decode-after-check bug: check-time and use-time operate on different representations of the same string.

Based on a proof of concept filed on huntr, the report compares payloads. nltk:../../../etc/passwd is blocked as designed; nltk:%2fetc%2fpasswd slips through and decodes to an absolute path; and a variant built from repeated %2e%2e segments walks five levels up and out of the data directory. A related payload, nltk:%2fproc%2fself%2fenviron, targets the process environment file, which frequently holds API keys, database credentials and cloud secrets passed in as variables.

An incomplete earlier fix

NLTK had already dealt with path traversal in GitHub issue #3504, and that mitigation is precisely the regex described above. The blocklist itself was accurate — literal traversal patterns, leading slashes and drive letters are all caught — but because it only ever saw the encoded form, an encoded copy of the same pattern slipped through.

The second line of defense is opt-in

NLTK also ships a nltk.pathsec module meant to re-check a path immediately before the file is opened. By default, though, it only raises an exception when the NLTK_PATHSEC_ENFORCE environment variable is explicitly set; otherwise it emits a RuntimeWarning and the open() call proceeds anyway. Out of the box, the one backstop that might have caught a bypassed regex amounts to little more than a log line.

Who is affected

The report flags any application that passes externally controlled input into load() or find() as the resource name: NLP web services and APIs that let users choose a corpus or model, hosted notebook services executing user-supplied code, multi-tenant ML pipelines that parameterize resource identifiers per tenant, and CI/CD pipelines that build resource paths from external input.

The CVSS vector (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) tells the story: confidentiality only. Nothing is modified or taken down, but an attacker gains read access to anything the process's user can read — beyond /etc/passwd and /proc/self/environ, that includes application config files, SSH private keys and locally cached cloud-metadata responses.

The fix in 3.10.0

Version 3.10.0 addresses the root cause by validating the decoded form. A new helper, _assert_no_encoded_bypass(), applies the same unsafe-pattern regex to a string after a single unquote() pass, mirroring the single decode that url2pathname() performs.

The write-up highlights three design decisions. The same regex is reused rather than a second blocklist being invented, leaving one policy to keep in sync. Decoding happens exactly once, because repeated decoding would corrupt legitimately double-encoded values such as %2520, a literal %20. And every entry point now runs the check — the rejection wrapper, the nltk: scheme handling inside normalize_resource_url(), and the defense-in-depth check inside find() — so no code path remains where a resource name turns into a file path without the decoded check running.

What to do

The primary recommendation is to upgrade to NLTK 3.10.0 or later. Where an immediate upgrade is not possible, setting NLTK_PATHSEC_ENFORCE=true turns the pathsec layer's warning into a hard block, but the report frames that as a stopgap mitigation rather than a substitute for patching.

Why it matters

NLTK sits underneath a large share of Python NLP work, and its resource loaders run every time a corpus or model is opened, so the vulnerable surface is broad. The CVSS vector also marks the issue as network-exploitable with no privileges or user interaction required, which matters most for services that accept user-supplied resource names. Beyond this one library, the bug is a clean illustration of a recurring failure mode: validating one representation of a string while using another. It also shows that a defense-in-depth layer which is not enforced by default provides no real protection — the pathsec module existed and could have stopped the bypass, but shipped as a warning. Teams running NLP services should treat unpatched deployments as an arbitrary-read primitive for the service account.

  • #nltk
  • #python
  • #nlp
  • #security
  • #path-traversal
  • #cve

Related posts