{"id":"CVE-2026-78682","aliases":["GHSA-6ww7-3frv-cqxh","PYSEC-2026-3733"],"url":"https://o3.security/vulnerability/CVE-2026-78682","summary":"NLTK before 3.10.3 SSRF Protection Bypass via Proxy","details":"### Summary\n\nCurrent 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.\n\n### Details\n\n- **Vulnerability type:** Server-side request forgery\n- **Affected component:** `nltk.pathsec.urlopen`, `nltk.data.load`, `nltk.downloader.Downloader.index`, `nltk.downloader.Downloader.download`\n- **Affected versions:** Current source `v3.10.0-rc2`; published `3.9.4` was a negative control and did not reproduce.\n- **Patched versions:** Not yet patched\n- **Root cause:** Proxy-handler inheritance disables `_SafeHTTPHandler` and `_SafeHTTPSHandler`, so the validated hostname no longer matches the actual egress destination.\n\nThe 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()`.\n\n### PoC\n\n**Preconditions**\n- The runtime has an HTTP proxy configured and the caller relies on `pathsec` to keep network fetches SSRF-safe.\n\n**Steps**\n1. Start a loopback-only HTTP server that serves secret text, a valid downloader index, and a ZIP payload.\n2. Configure a proxy that forwards a validated public URL to that internal loopback service.\n3. Call `pathsec.urlopen()` or `nltk.data.load()` on the public URL and observe the internal response is returned.\n4. Instantiate `Downloader(server_index_url=...)`, call `index()` and `download()`, and observe internal-only content is parsed and installed.\n\n**Minimal reproducible excerpt**\n\n```text\n{'urlopen': 'PROXY_TEXT_SECRET', 'data_load': 'PROXY_TEXT_SECRET', 'downloaded_file': 'INTERNAL_ZIP_SECRET'}\n```\n\n### Impact\n\nConsumers 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.\n\n### Remediation\n\nPreserve 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.\n\n### References\n\n- https://github.com/nltk/nltk/blob/v3.10.0-rc2/nltk/pathsec.py#L468-L518\n- https://github.com/nltk/nltk/blob/v3.10.0-rc2/nltk/data.py#L1247-L1283\n- https://github.com/nltk/nltk/blob/v3.10.0-rc2/nltk/downloader.py#L875-L889\n- https://github.com/nltk/nltk/blob/v3.10.0-rc2/nltk/downloader.py#L1220-L1226\n- https://github.com/nltk/nltk/blob/3.9.4/nltk/pathsec.py#L245-L250\n\n---\n\n## Fix + attack demonstration (verified)\n\n\nNLTK 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.\n\n## Attack demonstration (reproduced; captured output)\nA loopback HTTP server stands in for the internal target; `http_proxy` points at it; NLTK is asked for a **public** IP URL.\n\n**Before the fix** — the internal secret is exfiltrated through the proxy:\n```\nvalidate_network_url(public): PASSED\n*** BYPASS: pathsec.urlopen returned INTERNAL content via proxy: 'INTERNAL_ONLY_SECRET'\n```\n\n**After the fix** — five scenarios, isolated subprocesses:\n| Scenario | Result |\n|---|---|\n| proxied (env) + ENFORCE | `PermissionError` — **blocked** |\n| proxied + opt-in | returns secret — escape hatch works |\n| explicit `ProxyHandler` (not env) + ENFORCE | `PermissionError` — **blocked** (whole class) |\n| no proxy (direct) | internal IP still refused — pinning intact |\n| proxied + `ENFORCE=False` | returns secret **+ warns** |\n\n## Tests\n`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.\n\n## Note\nThe 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.","published":"2026-08-25T01:30:38.486Z","modified":"2026-10-10T02:30:29.962024337Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"PyPI","name":"nltk","fixedVersion":"3.10.3"}],"fix":{"url":"https://github.com/nltk/nltk/commit/767333a005a1cd3d82d2029215f2dbe66a5844d9","label":"nltk/nltk@767333a"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/78xxx/CVE-2026-78682.json"},{"type":"ADVISORY","url":"https://github.com/nltk/nltk/security/advisories/GHSA-6ww7-3frv-cqxh"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-78682"},{"type":"ADVISORY","url":"https://www.vulncheck.com/advisories/nltk-before-ssrf-protection-bypass-via-proxy"},{"type":"WEB","url":"https://github.com/nltk/nltk/commit/767333a005a1cd3d82d2029215f2dbe66a5844d9"},{"type":"PACKAGE","url":"https://github.com/nltk/nltk"},{"type":"WEB","url":"https://github.com/nltk/nltk/releases/tag/v3.10.3"},{"type":"WEB","url":"https://github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3733.yaml"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-10T02:30:29.962024337Z"}}