{"id":"CVE-2026-12259","kind":"vulnerability","aliases":["CVE-2026-12259","GHSA-5wp5-5229-5g6q"],"related":[],"title":"NLTK: Missing Post-Download Integrity Verification Allows Malicious Package Injection","description":"NLTK's package downloader in nltk/downloader.py does not verify file integrity after download and before extraction.\n\nThe download flow at lines 789-825:\n1. File is downloaded to a temp path via HTTP\n2. os.replace(tmp_filepath, filepath) moves it to the final location (line 799)\n3. Extraction begins via _unzip_iter() (line 825)\n\nBetween 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.\n\nAttack vectors:\n1. MITM during HTTP download (NLTK downloads from http:// by default on some mirrors)\n2. Race condition on shared filesystems (attacker replaces file between os.replace and _unzip_iter)\n3. DNS poisoning redirecting to attacker-controlled server\n\nPoC:\n```python\nimport nltk\nimport unittest.mock\nimport zipfile\nimport io\nimport os\n\n# Create a malicious zip that will be \"downloaded\"\nmalicious_zip = io.BytesIO()\nwith zipfile.ZipFile(malicious_zip, 'w') as zf:\n    zf.writestr('punkt_tab/tokenizers/punkt_tab/english.pickle', \n                b'MALICIOUS PAYLOAD - attacker controlled content')\n\n# Patch urllib to return our malicious zip\nwith unittest.mock.patch('urllib.request.urlopen') as mock_urlopen:\n    mock_response = unittest.mock.MagicMock()\n    mock_response.read.return_value = malicious_zip.getvalue()\n    mock_response.headers = {'Content-Length': str(len(malicious_zip.getvalue()))}\n    mock_urlopen.return_value = mock_response\n    \n    # Download proceeds, no integrity check catches the swap\n    # nltk.download('punkt_tab')  # Would install attacker payload\n```\n\nThis 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.\n\nSuggested 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.","summary":"NLTK's downloader does not verify a SHA-256 checksum on downloaded data packages before extracting them. An attacker who can tamper with a download, through a man-in-the-middle position, a plain-HTTP mirror, DNS poisoning, or a race on a shared filesystem, can get a malicious package installed in place of the real one.","severity":"medium","cvss":6.5,"epss":0.00101,"exploited":false,"exploited_at":null,"products":[{"name":"nltk","fixed":["3.9.3"],"affected":"<= 3.9.2","ecosystem":"pypi"},{"name":"nltk","fixed":[],"affected":"= 3.9.4","ecosystem":"vendor"}],"ecosystems":["pypi","vendor"],"tags":["ci","cwe-494","nltk","package","pypi","python","self-hosted"],"not_affected_if":["https-only-network","no-runtime-downloads","pre-bundled-data"],"how_to_check":"- Run `pip show nltk` and check whether the version is `3.9.2` or lower.\n- Check whether `nltk.download` fetches from any `http://` mirror in your environment.\n- Check whether NLTK data directories live on a shared or writable filesystem.","fix":"- Upgrade nltk to `3.9.3` or later with `pip install --upgrade nltk==3.9.3`.\n- Until the upgrade, download NLTK data only over HTTPS from trusted networks.","status":"open","published_at":"2026-08-03T08:17:17+00:00","first_seen_at":"2026-10-27T08:02:10+00:00","triaged_at":"2026-10-27T08:03:32+00:00","updated_at":"2026-10-27T08:31:47+00:00","category":"package","confidence":0.75,"triage":{"at":"2026-10-27T08:03:32Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade nltk to `3.9.3` or later with `pip install --upgrade nltk==3.9.3`.\n- Until the upgrade, download NLTK data only over HTTPS from trusted networks.","reach":"network","title":"NLTK downloader skips integrity check, letting tampered packages install","impact":"medium","summary":"NLTK's downloader does not verify a SHA-256 checksum on downloaded data packages before extracting them. An attacker who can tamper with a download, through a man-in-the-middle position, a plain-HTTP mirror, DNS poisoning, or a race on a shared filesystem, can get a malicious package installed in place of the real one.","category":"package","nicknames":[],"who_cares":["python","nltk","pypi","ci","self-hosted"],"confidence":0.75,"needs_auth":"no","worst_case":"Attacker-supplied files replace NLTK data, possibly enabling code execution on the host","analyst_note":"","how_to_check":"- Run `pip show nltk` and check whether the version is `3.9.2` or lower.\n- Check whether `nltk.download` fetches from any `http://` mirror in your environment.\n- Check whether NLTK data directories live on a shared or writable filesystem.","needs_analyst":false,"not_affected_if":["pre-bundled-data","no-runtime-downloads","https-only-network"],"who_is_affected":"Teams that run nltk.download at install or build time, or at runtime, on networks where traffic can be intercepted, or that use mirrors served over plain HTTP. Systems that shared a writable data directory are also exposed to the race-condition variant. Teams with NLTK data baked into a reviewed image and no runtime downloads have less exposure.","user_interaction":"yes","needs_special_setup":"unknown","severity_if_unknown":"unknown"},"cost_usd":0.001077,"input_tokens":4102,"output_tokens":1334,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-1015':169C '-12259':14A,23A '-14009':341C '-2024':332C '-2025':340C '-2026':13A,22A '-256':44B,158C,375C '-39705':333C '-494':28B '-5':19A '-825':117C '1':118C,207C '2':128C,151C,220C '3':141C,153C,234C '5229':18A '5wp5':17A '789':116C '799':140C '825':148C '982':168C 'actual':203C 'address':348C,355C 'allow':8A 'alreadi':184C 'attack':54B,205C,226C,240C,279C,325C 'attacker-control':239C 'b':276C 'begin':143C 'call':194C 'catch':317C 'check':180C,316C 'checksum':45B,161C,385C 'ci':26B 'compar':381C 'comput':373C 'condit':222C 'content':281C,303C 'content-length':302C 'control':241C,280C 'creat':255C 'cve':12A,21A,331C,339C 'cwe':27B 'data':48B 'date':190C 'default':216C 'delet':392C 'deseri':335C 'distinct':329C 'dns':74B,235C 'download':5A,38B,47B,60B,98C,107C,112C,121C,176C,196C,211C,213C,262C,312C,337C 'exist':163C 'expect':384C 'extract':51B,110C,142C,352C,359C 'file':104C,119C,200C,228C,379C,394C 'filepath':131C,132C 'filesystem':82B,225C 'final':137C,378C 'find':354C 'fix':366C 'flow':113C 'g6q':20A 'gap':357C 'get':84B 'ghsa':16A 'ghsa-5wp5':15A 'happen':350C 'hash':397C 'host':35B 'http':72B,127C,210C 'import':245C,247C,249C,251C,253C 'index':389C 'inject':11A 'instal':88B,185C,324C 'integr':6A,105C,315C,361C 'io':252C 'io.bytesio':265C 'iter':146C,233C,372C 'len':306C 'length':304C 'line':115C,139C,147C,167C 'locat':138C 'logic':162C 'malici':9A,86B,257C,263C,268C,277C,287C 'malicious_zip.getvalue':300C,307C 'man':64B 'man-in-the-middl':63B 'match':400C 'middl':67B 'mirror':73B,219C 'miss':2A 'mitm':208C 'mock':293C,295C,310C 'mock_response.headers':301C 'mock_response.read.return':298C 'mock_urlopen.return':308C 'move':133C 'never':193C,363C 'nltk':1A,24A,25A,29B,36B,95C,212C,246C 'nltk.download':320C 'nltk/downloader.py':100C 'one':94B 'os':254C 'os.replace':129C,230C,368C 'packag':10A,30B,49B,87B,97C,183C,388C 'patch':282C 'path':125C,345C 'payload':278C,326C 'pickl':334C 'pkg':165C 'place':90B 'plain':71B 'plain-http':70B 'poc':243C 'poison':75B,236C 'posit':68B 'post':4A 'post-download':3A 'proceed':313C 'punkt':274C,321C 'pypi':31B 'python':32B,244C 'race':78B,221C 'real':93B 'receiv':204C 'redirect':237C 'reject':390C 'replac':227C 'respons':296C,311C 'return':285C 'self':34B 'self-host':33B 'server':242C 'sha':43B,157C,374C 'share':81B,224C 'slip':344C 'status':166C,179C 'step':150C 'str':305C 'suggest':365C 'swap':319C 'tab':322C 'tab/tokenizers/punkt_tab/english.pickle':275C 'tamper':57B 'temp':124C 'tmp':130C 'travers':346C 'unittest.mock':248C 'unittest.mock.magicmock':297C 'unittest.mock.patch':290C 'unzip':145C,232C,371C 'up-to-d':187C 'urllib':283C 'urllib.request.urlopen':291C 'urlopen':294C 'use':174C 'valu':299C,309C 'vector':206C 'verif':7A,159C 'verifi':41B,103C,198C,364C 'via':126C,144C,336C 'w':270C 'would':323C 'zf':272C 'zf.writestr':273C 'zip':258C,264C,269C,288C,343C 'zip-slip':342C 'zipfil':250C 'zipfile.zipfile':267C","nicknames":[],"mitigations":[],"who_affected":"Teams that run nltk.download at install or build time, or at runtime, on networks where traffic can be intercepted, or that use mirrors served over plain HTTP. Systems that shared a writable data directory are also exposed to the race-condition variant. Teams with NLTK data baked into a reviewed image and no runtime downloads have less exposure.","tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-08-03T08:17:17+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-5wp5-5229-5g6q","url":"https://github.com/advisories/GHSA-5wp5-5229-5g6q","severity":"medium","published_at":"2026-09-08T15:27:55+00:00","modified_at":"2026-10-27T07:58:08+00:00","first_seen_at":"2026-10-27T08:02:10+00:00","fetched_at":"2026-10-27T08:02:10+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-12259","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-12259","severity":null,"published_at":"2026-08-03T08:17:17+00:00","modified_at":"2026-10-23T22:10:00+00:00","first_seen_at":"2026-10-27T08:31:47+00:00","fetched_at":"2026-10-27T08:31:47+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}