CVE-2014-1859: Improper Link Resolution Before File Access ('Link Following') (CWE-59)
value: (1) core/tests/test_memmap.py, (2) core/tests/test_multiarray.py, (3) f2py/f2py2e.py, and (4) lib/tests/test_io.py in NumPy before 1.8.1 allow local users to write to arbitrary files via a symlink attack on a temporary file.
CWE description: The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource.
CVSS v3 base score: 5.5
Published: 2018-01-08T19:29Z
Repository: https://github.com/numpy/numpy
Language: Python
Files changed: numpy/core/tests/test_memmap.py, numpy/core/tests/test_multiarray.py, numpy/f2py/__init__.py, numpy/f2py/f2py2e.py, numpy/lib/tests/test_io.py
Commit date: 2014-02-06 00:54:24 +0100
Commit message: ENH: remove insecure mktemp use

mktemp only returns a filename, a malicous user could replace it before
it gets used.

Diff: diff --git a/numpy/core/tests/test_memmap.py b/numpy/core/tests/test_memmap.py
index 6de6319ef..10e7a0817 100644
--- a/numpy/core/tests/test_memmap.py
+++ b/numpy/core/tests/test_memmap.py
@@ -1,9 +1,9 @@
 from __future__ import division, absolute_import, print_function
 
 import sys
-from tempfile import NamedTemporaryFile, TemporaryFile, mktemp
+from tempfile import NamedTemporaryFile, TemporaryFile
 import os
 
 from numpy import memmap
 from numpy import arange, allclose, asarray
 from numpy.testing import *
@@ -31,16 +31,15 @@ def test_roundtrip(self):
                        shape=self.shape)
         assert_(allclose(self.data, newfp))
         assert_array_equal(self.data, newfp)
 
     def test_open_with_filename(self):
-        tmpname = mktemp('', 'mmap')
-        fp = memmap(tmpname, dtype=self.dtype, mode='w+',
-                       shape=self.shape)
-        fp[:] = self.data[:]
-        del fp
-        os.unlink(tmpname)
+        with NamedTemporaryFile() as tmp:
+            fp = memmap(tmp.name, dtype=self.dtype, mode='w+',
+                        shape=self.shape)
+            fp[:] = self.data[:]
+            del fp
 
     def test_unnamed_file(self):
         with TemporaryFile() as f:
             fp = memmap(f, dtype=self.dtype, shape=self.shape)
             del fp
@@ -53,21 +52,20 @@ def test_attributes(self):
         self.assertEqual(offset, fp.offset)
         self.assertEqual(mode, fp.mode)
         del fp
 
     def test_filename(self):
-        tmpname = mktemp('', 'mmap')
-        fp = memmap(tmpname, dtype=self.dtype, mode='w+',
-                       shape=self.shape)
-        abspath = os.path.abspath(tmpname)
-        fp[:] = self.data[:]
-        self.assertEqual(abspath, fp.filename)
-        b = fp[:1]
-        self.assertEqual(abspath, b.filename)
-        del b
-        del fp
-        os.unlink(tmpname)
+        with NamedTemporaryFile() as tmp:
+            fp = memmap(tmp.name, dtype=self.dtype, mode='w+',
+                        shape=self.shape)
+            abspath = os.path.abspath(tmp.name)
+            fp[:] = self.data[:]
+            self.assertEqual(abspath, fp.filename)
+            b = fp[:1]
+            self.assertEqual(abspath, b.filename)
+            del b
+            del fp
 
     def test_filename_fileobj(self):
         fp = memmap(self.tmpfp, dtype=self.dtype, mode="w+",
                     shape=self.shape)
         self.assertEqual(fp.filename, self.tmpfp.name)
diff --git a/numpy/core/tests/test_multiarray.py b/numpy/core/tests/test_multiarray.py
index c2ac00923..a6f7b34b9 100644
--- a/numpy/core/tests/test_multiarray.py
+++ b/numpy/core/tests/test_multiarray.py
@@ -2314,16 +2314,15 @@ def setUp(self):
         shape = (2, 4, 3)
         rand = np.random.random
         self.x = rand(shape) + rand(shape).astype(np.complex)*1j
         self.x[0,:, 1] = [nan, inf, -inf, nan]
         self.dtype = self.x.dtype
-        self.filename = tempfile.mktemp()
+        self.file = tempfile.NamedTemporaryFile()
+        self.filename = self.file.name
 
     def tearDown(self):
-        if os.path.isfile(self.filename):
-            os.unlink(self.filename)
-            #tmp_file.close()
+        self.file.close()
 
     def test_bool_fromstring(self):
         v = np.array([True, False, True, False], dtype=np.bool_)
         y = np.fromstring('1 0 -2.3 0.0', sep=' ', dtype=np.bool_)
         assert_array_equal(v, y)
@@ -2347,11 +2346,10 @@ def test_roundtrip_file(self):
         # NB. doesn't work with flush+seek, due to use of C stdio
         f = open(self.filename, 'rb')
         y = np.fromfile(f, dtype=self.dtype)
         f.close()
         assert_array_equal(y, self.x.flat)
-        os.unlink(self.filename)
 
     def test_roundtrip_filename(self):
         self.x.tofile(self.filename)
         y = np.fromfile(self.filename, dtype=self.dtype)
         assert_array_equal(y, self.x.flat)
@@ -2400,12 +2398,10 @@ def test_file_position_after_fromfile(self):
                 np.fromfile(f, dtype=np.float64, count=1)
                 pos = f.tell()
                 f.close()
                 assert_equal(pos, 10, err_msg=err_msg)
 
-        os.unlink(self.filename)
-
     def test_file_position_after_tofile(self):
         # gh-4118
         sizes = [io.DEFAULT_BUFFER_SIZE//8,
                  io.DEFAULT_BUFFER_SIZE,
                  io.DEFAULT_BUFFER_SIZE*8]
@@ -2429,12 +2425,10 @@ def test_file_position_after_tofile(self):
             np.array([0], dtype=np.float64).tofile(f)
             pos = f.tell()
             f.close()
             assert_equal(pos, 10, err_msg=err_msg)
 
-        os.unlink(self.filename)
-
     def _check_from(self, s, value, **kw):
         y = np.fromstring(asbytes(s), **kw)
         assert_array_equal(y, value)
 
         f = open(self.filename, 'wb')
@@ -2533,11 +2527,10 @@ def test_tofile_sep(self):
         f.close()
         f = open(self.filename, 'r')
         s = f.read()
         f.close()
         assert_equal(s, '1.51,2.0,3.51,4.0')
-        os.unlink(self.filename)
 
     def test_tofile_format(self):
         x = np.array([1.51, 2, 3.51, 4], dtype=float)
         f = open(self.filename, 'w')
         x.tofile(f, sep=',', format='%.2f')
diff --git a/numpy/f2py/__init__.py b/numpy/f2py/__init__.py
index ccdbd4e0b..fcfd1853e 100644
--- a/numpy/f2py/__init__.py
+++ b/numpy/f2py/__init__.py
@@ -26,24 +26,24 @@ def compile(source,
     Read the source of this function for more information.
     '''
     from numpy.distutils.exec_command import exec_command
     import tempfile
     if source_fn is None:
-        fname = os.path.join(tempfile.mktemp()+'.f')
+        f = tempfile.NamedTemporaryFile(suffix='.f')
     else:
-        fname = source_fn
-
-    f = open(fname, 'w')
-    f.write(source)
-    f.close()
-
-    args = ' -c -m %s %s %s'%(modulename, fname, extra_args)
-    c = '%s -c "import numpy.f2py as f2py2e;f2py2e.main()" %s' %(sys.executable, args)
-    s, o = exec_command(c)
-    if source_fn is None:
-        try: os.remove(fname)
-        except OSError: pass
+        f = open(source_fn, 'w')
+
+    try:
+        f.write(source)
+        f.flush()
+
+        args = ' -c -m %s %s %s'%(modulename, f.name, extra_args)
+        c = '%s -c "import numpy.f2py as f2py2e;f2py2e.main()" %s' % \
+                (sys.executable, args)
+        s, o = exec_command(c)
+    finally:
+        f.close()
     return s
 
 from numpy.testing import Tester
 test = Tester().test
 bench = Tester().bench
diff --git a/numpy/f2py/f2py2e.py b/numpy/f2py/f2py2e.py
index ff9d19e9d..25407d421 100755
--- a/numpy/f2py/f2py2e.py
+++ b/numpy/f2py/f2py2e.py
@@ -89,11 +89,11 @@
 
   --[no-]lower     Do [not] lower the cases in <fortran files>. By default,
                    --lower is assumed with -h key, and --no-lower without -h key.
 
   --build-dir <dirname>  All f2py generated files are created in <dirname>.
-                   Default is tempfile.mktemp().
+                   Default is tempfile.mkdtemp().
 
   --overwrite-signature  Overwrite existing signature file.
 
   --[no-]latex-doc Create (or not) <modulename>module.tex.
                    Default is --no-latex-doc.
@@ -422,11 +422,11 @@ def run_compile():
         build_dir = sys.argv[i+1]
         del sys.argv[i+1]
         del sys.argv[i]
     else:
         remove_build_dir = 1
-        build_dir = os.path.join(tempfile.mktemp())
+        build_dir = tempfile.mkdtemp()
 
     _reg1 = re.compile(r'[-][-]link[-]')
     sysinfo_flags = [_m for _m in sys.argv[1:] if _reg1.match(_m)]
     sys.argv = [_m for _m in sys.argv if _m not in sysinfo_flags]
     if sysinfo_flags:
diff --git a/numpy/lib/tests/test_io.py b/numpy/lib/tests/test_io.py
index e3ccb391c..2ee5c838e 100644
--- a/numpy/lib/tests/test_io.py
+++ b/numpy/lib/tests/test_io.py
@@ -2,11 +2,13 @@
 
 import sys
 import gzip
 import os
 import threading
-from tempfile import mkstemp, mktemp, NamedTemporaryFile
+import shutil
+import contextlib
+from tempfile import mkstemp, mkdtemp, NamedTemporaryFile
 import time
 import warnings
 import gc
 from io import BytesIO
 from datetime import datetime
@@ -19,10 +21,16 @@
 from nose import SkipTest
 from numpy.ma.testutils import (TestCase, assert_equal, assert_array_equal,
                                 assert_raises, run_module_suite)
 from numpy.testing import assert_warns, assert_, build_err_msg
 
+@contextlib.contextmanager
+def tempdir(change_dir=False):
+    tmpdir = mkdtemp()
+    yield tmpdir
+    shutil.rmtree(tmpdir)
+
 
 class TextIO(BytesIO):
     """Helper IO class.
 
     Writes encode strings to bytes if needed, reads return bytes.
@@ -175,18 +183,18 @@ def roundtrip(self, *args, **kwargs):
 
     @np.testing.dec.skipif(not IS_64BIT, "Works only with 64bit systems")
     @np.testing.dec.slow
     def test_big_arrays(self):
         L = (1 << 31) + 100000
-        tmp = mktemp(suffix='.npz')
         a = np.empty(L, dtype=np.uint8)
-        np.savez(tmp, a=a)
-        del a
-        npfile = np.load(tmp)
-        a = npfile['a']
-        npfile.close()
-        os.remove(tmp)
+        with tempdir() as tmpdir:
+            tmp = open(os.path.join(tmpdir, "file.npz"), "w")
+            np.savez(tmp, a=a)
+            del a
+            npfile = np.load(tmp)
+            a = npfile['a']
+            npfile.close()
 
     def test_multiple_arrays(self):
         a = np.array([[1, 2], [3, 4]], float)
         b = np.array([[1 + 2j, 2 + 7j], [3 - 6j, 4 + 12j]], complex)
         self.roundtrip(a, b)

Vulnerable code: from tempfile import NamedTemporaryFile, TemporaryFile, mktemp
        tmpname = mktemp('', 'mmap')
        fp = memmap(tmpname, dtype=self.dtype, mode='w+',
                       shape=self.shape)
        fp[:] = self.data[:]
        del fp
        os.unlink(tmpname)
        tmpname = mktemp('', 'mmap')
        fp = memmap(tmpname, dtype=self.dtype, mode='w+',
                       shape=self.shape)
        abspath = os.path.abspath(tmpname)
        fp[:] = self.data[:]
        self.assertEqual(abspath, fp.filename)
        b = fp[:1]
        self.assertEqual(abspath, b.filename)
        del b
        del fp
        os.unlink(tmpname)
        self.filename = tempfile.mktemp()
        if os.path.isfile(self.filename):
            os.unlink(self.filename)
            #tmp_file.close()
        os.unlink(self.filename)
        os.unlink(self.filename)

        os.unlink(self.filename)

        os.unlink(self.filename)
        fname = os.path.join(tempfile.mktemp()+'.f')
        fname = source_fn

    f = open(fname, 'w')
    f.write(source)
    f.close()

    args = ' -c -m %s %s %s'%(modulename, fname, extra_args)
    c = '%s -c "import numpy.f2py as f2py2e;f2py2e.main()" %s' %(sys.executable, args)
    s, o = exec_command(c)
    if source_fn is None:
        try: os.remove(fname)
        except OSError: pass
                   Default is tempfile.mktemp().
        build_dir = os.path.join(tempfile.mktemp())
from tempfile import mkstemp, mktemp, NamedTemporaryFile
        tmp = mktemp(suffix='.npz')
        np.savez(tmp, a=a)
        del a
        npfile = np.load(tmp)
        a = npfile['a']
        npfile.close()
        os.remove(tmp)
Fixed code: from tempfile import NamedTemporaryFile, TemporaryFile
        with NamedTemporaryFile() as tmp:
            fp = memmap(tmp.name, dtype=self.dtype, mode='w+',
                        shape=self.shape)
            fp[:] = self.data[:]
            del fp
        with NamedTemporaryFile() as tmp:
            fp = memmap(tmp.name, dtype=self.dtype, mode='w+',
                        shape=self.shape)
            abspath = os.path.abspath(tmp.name)
            fp[:] = self.data[:]
            self.assertEqual(abspath, fp.filename)
            b = fp[:1]
            self.assertEqual(abspath, b.filename)
            del b
            del fp
        self.file = tempfile.NamedTemporaryFile()
        self.filename = self.file.name
        self.file.close()
        f = tempfile.NamedTemporaryFile(suffix='.f')
        f = open(source_fn, 'w')

    try:
        f.write(source)
        f.flush()

        args = ' -c -m %s %s %s'%(modulename, f.name, extra_args)
        c = '%s -c "import numpy.f2py as f2py2e;f2py2e.main()" %s' % \
                (sys.executable, args)
        s, o = exec_command(c)
    finally:
        f.close()
                   Default is tempfile.mkdtemp().
        build_dir = tempfile.mkdtemp()
import shutil
import contextlib
from tempfile import mkstemp, mkdtemp, NamedTemporaryFile
@contextlib.contextmanager
def tempdir(change_dir=False):
    tmpdir = mkdtemp()
    yield tmpdir
    shutil.rmtree(tmpdir)

        with tempdir() as tmpdir:
            tmp = open(os.path.join(tmpdir, "file.npz"), "w")
            np.savez(tmp, a=a)
            del a
            npfile = np.load(tmp)
            a = npfile['a']
            npfile.close()