GHSA-6ww7-3frv-cqxh CVE-2026-78682 PYSEC-2026-3733
NLTK: pathsec SSRF protection can be bypassed when a proxy is configured
- Severity
- 0.0 / 10
- High · CVSS 4.0
- Exploit chance (EPSS)
- 0.43%
- Chance of exploitation in the next 30 days. Higher than 37.78% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-441 Unintended Proxy or Intermediary ('Confused Deputy')
- CWE-918 Server-Side Request Forgery (SSRF)
- Fix
3.10.3- Upgrade nltk to this version or later
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; published3.9.4was a negative control and did not reproduce. - Patched versions: Not yet patched
- Root cause: Proxy-handler inheritance disables
_SafeHTTPHandlerand_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
pathsecto keep network fetches SSRF-safe.
Steps
- Start a loopback-only HTTP server that serves secret text, a valid downloader index, and a ZIP payload.
- Configure a proxy that forwards a validated public URL to that internal loopback service.
- Call
pathsec.urlopen()ornltk.data.load()on the public URL and observe the internal response is returned. - Instantiate
Downloader(server_index_url=...), callindex()anddownload(), and observe internal-only content is parsed and installed.
Minimal reproducible excerpt
{'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.
Affected versions
PyPI
| Package | Vulnerable | Fixed in |
|---|---|---|
| nltk | < 3.10.3 |
3.10.3 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
nltk PyPI → 3.10.3
- pip
pip install --upgrade "nltk>=3.10.3"- poetry
poetry add "nltk>=3.10.3"- uv
uv add "nltk>=3.10.3"
Severity breakdown
CVSS 4.0 0.0
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
- Attack vector
- Network
- Attack complexity
- Low
- Attack requirements
- None
- Privileges required
- None
- User interaction
- None
- Confidentiality (vulnerable system)
- High
- Integrity (vulnerable system)
- None
- Availability (vulnerable system)
- None
- Confidentiality (subsequent systems)
- None
- Integrity (subsequent systems)
- None
- Availability (subsequent systems)
- None
CVSS 3.1 7.5
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- None
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- High
- Integrity
- None
- Availability
- None
Weakness
CWE-441: Unintended Proxy or Intermediary ('Confused Deputy')
The product receives a request, message, or directive from an upstream component, but the product does not sufficiently preserve the original source of the request before forwarding the request to an external actor that is outside of the product's control sphere. This causes the product to appear to be the source of the request, leading it to act as a proxy or other intermediary between the upstream component and the external actor.
How to prevent it
- Architecture and Design: Enforce the use of strong mutual authentication mechanism between the two parties.
- Architecture and Design: Whenever a product is an intermediary or proxy for transactions between two other components, the proxy core should not drop the identity of the initiator of the transaction. The immutability of the identity of the initiator must be maintained and should be forwarded all the way to the target.
CWE-918: Server-Side Request Forgery (SSRF)
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
References
Advisories
Packages
Other links
- github.com/nltk/nltk/security/advisories/GHSA-6ww7-3frv-cqxh
- github.com/nltk/nltk/commit/767333a005a1cd3d82d2029215f2dbe66a5844d9
- github.com/nltk/nltk/releases/tag/v3.10.3
- github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3733.yaml
- vulncheck.com/advisories/nltk-before-ssrf-protection-bypass-via-proxy
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-j456-xh4h-cpf2 NLTK: WekaClassifier passes an unvalidated caller-controlled model path to the weka JVM, bypassing nltk.pathsec containment | High | Also affects nltk |
| GHSA-ff5c-cp5c-9wjf NLTK: Uncontrolled resource consumption in RecursiveDescentParser via ambiguous or left-recursive grammars | Medium | Also affects nltk |
| GHSA-f794-5jv7-7672 NLTK: Downloader.download follows hardlinks and overwrites outside-root files | High | Also affects nltk |
| GHSA-8mgp-746c-j5xp NLTK: Model-artifact APIs bypass pathsec and touch files outside allowed roots | High | Also affects nltk |
| GHSA-8mpw-7fpc-4gqj NLTK: Pl196xCorpusReader has quadratic ReDoS on malformed TEI blocks | Low | Also affects nltk |
| GHSA-cw6x-m8jw-qmrh NLTK: Uncontrolled recursion in nltk.featstruct.FeatStructReader causes unhandled RecursionError (DoS) via deeply nested feature-structure input | Medium | Also affects nltk |
| GHSA-vp2x-qp44-57v7 NLTK: Quadratic CPU Exhaustion in `XMLCorpusView._read_xml_fragment()` | Low | Also affects nltk |
| GHSA-ww6m-cw3f-q94g NLTK: Quadratic-time DoS in PorterStemmer via long runs of 'y' | High | Also affects nltk |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor