Vulnerability: Path Traversal in nltk NLTK Downloader before 3.4.5 is vulnerable to a directory traversal, allowing attackers to write arbitrary files via a ../ (dot dot slash) in an NLTK package (ZIP archive) that is mishandled during extraction.
Vulnerability ID: GHSA-mr7p-25v2-35wr
CWE: CWE-22
Severity score: 7.5
Project: https://github.com/nltk/nltk
Published: 2019-08-23
File: nltk/downloader.py
Commit message: CVE-2019-14751:
Fixed security bug in downloader
(https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-14751)
Commit author: Steven Bird

Vulnerable version (before fix):
 def _unzip_iter(filename, root, verbose=True):         yield ErrorMessage(filename, e)         return      # Get lists of directories & files     namelist = zf.namelist()     dirlist = set()     for x in namelist:         if x.endswith('/'):             dirlist.add(x)         else:             dirlist.add(x.rsplit('/', 1)[0] + '/')     filelist = [x for x in namelist if not x.endswith('/')]      # Create the target directory if it doesn't exist     if not os.path.exists(root):         os.mkdir(root)      # Create the directory structure     for dirname in sorted(dirlist):         pieces = dirname[:-1].split('/')         for i in range(len(pieces)):             dirpath = os.path.join(root, *pieces[: i + 1])             if not os.path.exists(dirpath):                 os.mkdir(dirpath)      # Extract files.     for i, filename in enumerate(filelist):         filepath = os.path.join(root, *filename.split('/'))          try:             with open(filepath, 'wb') as dstfile, zf.open(filename) as srcfile:                 shutil.copyfileobj(srcfile, dstfile)         except Exception as e:             yield ErrorMessage(filename, e)             return          if verbose and (i * 10 / len(filelist) > (i - 1) * 10 / len(filelist)):             sys.stdout.write('.')             sys.stdout.flush()     if verbose:         print() 

Fixed version (after fix):
 def _unzip_iter(filename, root, verbose=True):         yield ErrorMessage(filename, e)         return      zf.extractall(root)      if verbose:         print() 

Patch:
patch: @@ -2260,42 +2260,8 @@ def _unzip_iter(filename, root, verbose=True):
         yield ErrorMessage(filename, e)
         return
 
-    # Get lists of directories & files
-    namelist = zf.namelist()
-    dirlist = set()
-    for x in namelist:
-        if x.endswith('/'):
-            dirlist.add(x)
-        else:
-            dirlist.add(x.rsplit('/', 1)[0] + '/')
-    filelist = [x for x in namelist if not x.endswith('/')]
-
-    # Create the target directory if it doesn't exist
-    if not os.path.exists(root):
-        os.mkdir(root)
-
-    # Create the directory structure
-    for dirname in sorted(dirlist):
-        pieces = dirname[:-1].split('/')
-        for i in range(len(pieces)):
-            dirpath = os.path.join(root, *pieces[: i + 1])
-            if not os.path.exists(dirpath):
-                os.mkdir(dirpath)
-
-    # Extract files.
-    for i, filename in enumerate(filelist):
-        filepath = os.path.join(root, *filename.split('/'))
-
-        try:
-            with open(filepath, 'wb') as dstfile, zf.open(filename) as srcfile:
-                shutil.copyfileobj(srcfile, dstfile)
-        except Exception as e:
-            yield ErrorMessage(filename, e)
-            return
+    zf.extractall(root)
 
-        if verbose and (i * 10 / len(filelist) > (i - 1) * 10 / len(filelist)):
-            sys.stdout.write('.')
-            sys.stdout.flush()
     if verbose:
         print()
path: nltk/downloader.py