NLTK: pathsec SSRF protection can be bypassed when a proxy is configured
HighCVE-2026-78682 · Published Sep 8, 2026
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| nltk PyPI | < 3.10.3 | 3.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
- github.com/nltk/nltk/security/advisories/GHSA-6ww7-3frv-cqxh
- nvd.nist.gov/vuln/detail/CVE-2026-78682
- github.com/nltk/nltk/commit/767333a005a1cd3d82d2029215f2dbe66a5844d9
- github.com/nltk/nltk
- github.com/nltk/nltk/releases/tag/v3.10.3
- github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3733.yaml
- www.vulncheck.com/advisories/nltk-before-ssrf-protection-bypass-via-proxy
More NLTK advisories
All NLTK| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Sep 8 | NLTK: Symlink escape in CorpusReader allows arbitrary local file read outside the corpus root CVE-2026-70626High6.2fixed in 3.9.4 | High6.2 | 3.9.4 |
| Sep 8 | NLTK: FileSystemPathPointer.open() sandbox check is dead code , arbitrary file read via file:// protocol CVE-2026-65915Medium6.5fixed in 3.10.0 | Medium6.5 | 3.10.0 |
| Sep 8 | NLTK: StreamBackedCorpusView Bypasses pathsec.ENFORCE - Arbitrary Local File Read CVE-2026-63312Highfixed in 3.10.0 | High | 3.10.0 |
| Sep 8 | NLTK: Missing Post-Download Integrity Verification Allows Malicious Package Injection CVE-2026-12259Medium5.3fixed in 3.9.3 | Medium5.3 | 3.9.3 |
| Sep 8 | NLTK: Stable FrameNet and NKJP readers parse outside-root XML CVE-2026-62385High5.9fixed in 3.10.0 | High5.9 | 3.10.0 |
| Sep 8 | NLTK: Symlink-based sandbox bypass in FramenetCorpusReader (bypasses the fix for CVE-2026-54292) CVE-2026-62384High7.5fixed in 3.10.2 | High7.5 | 3.10.2 |