GitHub Advisoryadvisory · PyPI
GHSA-6ww7-3frv-cqxh NLTK: pathsec SSRF protection can be bypassed when a proxy is configured
HIGH
Official page ↗
- Aliases
- CVE-2026-78682
- Ecosystem
- PyPI
- Packages
- nltk
- Severity
- High
- CVSS vector
- 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
- Weaknesses
- CWE-441, CWE-918
- Reviewed
- 2026-09-08
Affected packages
• PyPI nltk: < 3.10.3 → fixed in 3.10.3
Description
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 | `PermissionError` — blocked |
| proxied + opt-in | returns secret — escape hatch works |
| explicit `ProxyHandler` (not env) + ENFORCE | `PermissionError` — blocked (whole class) |
| no proxy (direct) | internal IP still refused — pinning intact |
| proxied + `ENFORCE=False` | returns secret + warns** |
Tests
`nltk/test/unit/test_pathsec.py`: 64 passed. Added an end-to-end regression (`test_proxied_fetch_does_not_reach_internal_target`) plus `test_env_proxy_fails_closed_under_enforce`; the prior `test_env_proxy_skips_pinning_handlers` (which encoded the vulnerable path) is re-expressed as the opt-in case. Existing direct-path DNS-rebinding and IP-policy tests unchanged and passing. pre-commit (isort/black/ruff) clean.
Note
The upfront `validate_network_url()` and the direct-path IP pinning (from the earlier DNS-rebinding fixes, CVE-2026-54296 / GHSA-qvv7) are unchanged — this only closes the proxied downgrade they didn't cover.
Related
Alias · CVE (1)
- CVE-2026-78682 NLTK before 3.10.3 contains a server-side request forgery vulnerability in nltk.pathsec.urlopen (and callers nltk.data.load,… vulnerability
References
- https://github.com/nltk/nltk/security/advisories/GHSA-6ww7-3frv-cqxh web
- https://nvd.nist.gov/vuln/detail/CVE-2026-78682 advisory
- https://github.com/nltk/nltk/commit/767333a005a1cd3d82d2029215f2dbe66a5844d9 web
- https://github.com/nltk/nltk package
- https://github.com/nltk/nltk/releases/tag/v3.10.3 web
- https://github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3733.yaml web
- https://www.vulncheck.com/advisories/nltk-before-ssrf-protection-bypass-via-proxy web