Résumé
NLTK: Missing Post-Download Integrity Verification Allows Malicious Package Injection
Détails de l’avis
NLTK's package downloader in nltk/downloader.py does not verify file integrity after download and before extraction.
The download flow at lines 789-825:
- File is downloaded to a temp path via HTTP
- os.replace(tmp_filepath, filepath) moves it to the final location (line 799)
- Extraction begins via _unzip_iter() (line 825)
Between steps 2 and 3, there is no SHA-256 verification. The checksum logic exists in _pkg_status() (lines 982-1015) but it is only used BEFORE download as a status check ("is this package already installed and up-to-date?"). It is never called after download to verify the file that was actually received.
Attack vectors:
- MITM during HTTP download (NLTK downloads from http:// by default on some mirrors)
- Race condition on shared filesystems (attacker replaces file between os.replace and _unzip_iter)
- DNS poisoning redirecting to attacker-controlled server
PoC:
import nltk
import unittest.mock
import zipfile
import io
import os
# Create a malicious zip that will be "downloaded"
malicious_zip = io.BytesIO()
with zipfile.ZipFile(malicious_zip, 'w') as zf:
zf.writestr('punkt_tab/tokenizers/punkt_tab/english.pickle',
b'MALICIOUS PAYLOAD - attacker controlled content')
# Patch urllib to return our malicious zip
with unittest.mock.patch('urllib.request.urlopen') as mock_urlopen:
mock_response = unittest.mock.MagicMock()
mock_response.read.return_value = malicious_zip.getvalue()
mock_response.headers = {'Content-Length': str(len(malicious_zip.getvalue()))}
mock_urlopen.return_value = mock_response
# Download proceeds, no integrity check catches the swap
# nltk.download('punkt_tab') # Would install attacker payload
This is distinct from CVE-2024-39705 (pickle deserialization via download) and CVE-2025-14009 (zip-slip path traversal). Those address what happens AFTER extraction. This finding addresses the gap BEFORE extraction where integrity is never verified.
Suggested fix: After os.replace() and before _unzip_iter(), compute SHA-256 of the final file and compare against the expected checksum from the package index. Reject and delete the file if the hash does not match.
Références
- https://github.com/advisories/GHSA-5wp5-5229-5g6q
- https://github.com/nltk/nltk/security/advisories/GHSA-5wp5-5229-5g6q
- https://nvd.nist.gov/vuln/detail/CVE-2026-12259
- https://github.com/nltk/nltk/pull/3449
- https://github.com/nltk/nltk/commit/0e26734a61094b628d93e26dc18dd7302567ac46
- https://github.com/nltk/nltk/releases/tag/3.9.3
- https://github.com/pypa/advisory-database/tree/main/vulns/nltk/PYSEC-2026-3729.yaml
- https://huntr.com/bounties/659ccf6d-12d4-4d4a-84c0-078633c35a5d
Vulnérabilités liées
Tout Supply chain →- HIGHCVE-2026-86063
Claude Code: Plugin Hash Pin Bypass Allows Bypass of Hash Pinning
- HIGHCVE-2026-107812
Nginx UI: Self-upgrade runs an unsigned binary verified only by a same-origin digest → RCE via a compromised mirror or MITM
- HIGHCVE-2026-102930
virtualenv: Downloaded seed wheels (pip/setuptools) are not integrity-checked before use
- HIGHSC-NOTEPADPP-CHRYSALIS-2025
In 2025, a Chinese state-linked espionage group hijacked the update channel of Notepad++, one of the most widely installed Windows text editors, to plant a custom backdoor on a handful of carefully chosen targets. The attackers never touched Notepad++'s source code or its signing keys. Instead they compromised the third-party shared-hosting server that delivered updates, and abused the fact that the WinGUp updater shipped without any integrity check on what it downloaded (tracked as CVE-2025-15556). For machines on their target list, WinGUp was silently redirected to attacker-controlled servers that served a trojanized installer; everyone else kept receiving the genuine update. The campaign ran from roughly June to December 2025 and was only made public in February 2026 by Rapid7, with Kaspersky and Broadcom/Symantec corroborating it independently.
- HIGHSC-ESCAN-RELOAD-2026
On or around January 20, 2026, attackers who had broken into a regional update server for eScan, the antivirus product made by India's MicroWorld Technologies, pushed a trojanized Reload.exe through the legitimate update channel during a window of roughly two hours. The security software itself became the delivery vehicle for malware. The malicious binary carried eScan's code-signing identity, but its signature was actually invalid; it ran anyway because the updater trusted the channel rather than rigorously validating the signature. Once on a host, the malware rewrote the Windows HOSTS file to cut eScan off from its own update servers, blinded script scanning, and reached out to takedown-resistant command-and-control hosted on blockchain naming systems. Morphisec disclosed the campaign, with Kaspersky and Darktrace corroborating it independently.
- CRITICALCVE-2019-15107
Disclosed in August 2019, CVE-2019-15107 was an unauthenticated remote code execution backdoor in Webmin, a widely deployed web-based system administration tool that runs with root privileges. The backdoor existed in the password_change.cgi feature: a Perl qx() statement passed the unsanitized old (and in some versions expired) parameter from the password-change request straight to a shell, letting an unauthenticated attacker run arbitrary commands as root, with version 1.890 exploitable in its default configuration and 1.900 through 1.920 exploitable when password expiry was enabled. Critically, the malicious code was never present in Webmin's GitHub source, which remained clean; it was inserted directly into the build infrastructure that produced the official SourceForge release packages, so users who installed signed official builds were backdoored while anyone auditing the public Git source saw nothing wrong. Webmin later confirmed the code was added on its build server on two separate occasions, in April 2018 producing the 1.890 release and again in July 2018 reintroducing it into 1.900 through 1.920, meaning backdoored builds were distributed for over a year. The project released 1.930 on August 17, 2019 to remove the backdoor.