Skip to content
NLTKGHSA-6ww7-3frv-cqxh

NLTK: pathsec SSRF protection can be bypassed when a proxy is configured

HighCVE-2026-78682 · Published Sep 8, 2026

GitHub advisory

Affected versions

PackageAffectedFixed in
nltk
PyPI
< 3.10.33.10.3
Details and references

### Summary Current NLTK source reopens SSRF in proxied environments. `pathsec.urlopen()` validates the requested hostname locally, but once proxy inheritance is enabled the real fetch is performed by the proxy rather than by the validated direct-connect socket path. ### Details - **Vulnerability type:** Server-side request forgery - **Affected component:** `nltk.pathsec.urlopen`, `nltk.data.load`, `nltk.downloader.Downloader.index`, `nltk.downloader.Downloader.download` - **Affected versions:** Current source `v3.10.0-rc2`; published `3.9.4` was a negative control and did not reproduce. - **Patched versions:** Not yet patched - **Root cause:** Proxy-handler inheritance disables `_SafeHTTPHandler` and `_SafeHTTPSHandler`, so the validated hostname no longer matches the actual egress destination. The hardened direct path pins the validated numeric destination IP before opening the socket. The proxied branch instead copies `ProxyHandler` instances from the global opener, marks the request as proxied, and skips the pinned handlers. I confirmed that a validated public URL can be fetched from a loopback-only internal service through the proxy path via `pathsec.urlopen()`, `nltk.data.load()`, `Downloader.index()`, and `Downloader.download()`. ### PoC **Preconditions** - The runtime has an HTTP proxy configured and the caller relies on `pathsec` to keep network fetches SSRF-safe. **Steps** 1. Start a loopback-only HTTP server that serves secret text, a valid downloader index, and a ZIP payload. 2. Configure a proxy that forwards a validated public URL to that internal loopback service. 3. Call `pathsec.urlopen()` or `nltk.data.load()` on the public URL and observe the internal response is returned. 4. Instantiate `Downloader(server_index_url=...)`, call `index()` and `download()`, and observe internal-only content is parsed and installed. **Minimal reproducible excerpt** ```text {'urlopen': 'PROXY_TEXT_SECRET', 'data_load': 'PROXY_TEXT_SECRET', 'downloaded_file': 'INTERNAL_ZIP_SECRET'} ``` ### Impact Consumers that trust `pathsec` as an SSRF barrier in proxied environments can be made to read internal-only HTTP resources, load forged downloader indexes, and install attacker-chosen package content fetched from the proxy's network view. ### Remediation Preserve destination validation for the actual proxy egress target or fail closed when the request would otherwise downgrade into an unpinned proxied path. Add regression tests across `pathsec.urlopen`, `nltk.data.load`, and downloader fetches with a configured proxy. ### References - https://github.com/nltk/nltk/blob/v3.10.0-rc2/nltk/pathsec.py#L468-L518 - https://github.com/nltk/nltk/blob/v3.10.0-rc2/nltk/data.py#L1247-L1283 - https://github.com/nltk/nltk/blob/v3.10.0-rc2/nltk/downloader.py#L875-L889 - https://github.com/nltk/nltk/blob/v3.10.0-rc2/nltk/downloader.py#L1220-L1226 - https://github.com/nltk/nltk/blob/3.9.4/nltk/pathsec.py#L245-L250 --- ## Fix + attack demonstration (verified) NLTK cannot pin the egress through a proxy, so it stops pretending to: under `ENFORCE` a proxied fetch is **refused** rather than performed unvalidated. Operators who trust their proxy opt back in with `NLTK_ALLOW_PROXIED_URLOPEN=1` or `nltk.pathsec.ALLOW_PROXIED_FETCH=True`; under `ENFORCE=False` the refusal degrades to a warning. This closes the **whole class** (environment proxies and explicit `ProxyHandler` alike), because NLTK declines any fetch whose egress it cannot validate. ## Attack demonstration (reproduced; captured output) A loopback HTTP server stands in for the internal target; `http_proxy` points at it; NLTK is asked for a **public** IP URL. **Before the fix** , the internal secret is exfiltrated through the proxy: ``` validate_network_url(public): PASSED *** BYPASS: pathsec.urlopen returned INTERNAL content via proxy: 'INTERNAL_ONLY_SECRET' ``` **After the fix** , five scenarios, isolated subprocesses: | Scenario | Result | |---|---| | proxied (env) + ENFORCE |

CVSS 4.0
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
Severity from
GitHub (reviewed advisory)
Weakness
CWE-441, CWE-918
Also known as
CVE-2026-78682, PYSEC-2026-3733

More NLTK advisories

All NLTK
DateAdvisory
Sep 8NLTK: Symlink escape in CorpusReader allows arbitrary local file read outside the corpus root
CVE-2026-70626High6.2fixed in 3.9.4
Sep 8NLTK: FileSystemPathPointer.open() sandbox check is dead code , arbitrary file read via file:// protocol
CVE-2026-65915Medium6.5fixed in 3.10.0
Sep 8NLTK: StreamBackedCorpusView Bypasses pathsec.ENFORCE - Arbitrary Local File Read
CVE-2026-63312Highfixed in 3.10.0
Sep 8NLTK: Missing Post-Download Integrity Verification Allows Malicious Package Injection
CVE-2026-12259Medium5.3fixed in 3.9.3
Sep 8NLTK: Stable FrameNet and NKJP readers parse outside-root XML
CVE-2026-62385High5.9fixed in 3.10.0
Sep 8NLTK: Symlink-based sandbox bypass in FramenetCorpusReader (bypasses the fix for CVE-2026-54292)
CVE-2026-62384High7.5fixed in 3.10.2

Critical advisories by email

Wednesdays: the week’s critical and high advisories in the AI and data stack, with the fixed versions. Only in weeks that have some.

Double opt-in. Unsubscribe any time.