CVE-2013-4251 (Python, published 2019-11-04T20:15Z)
Description: value: The scipy.weave component in SciPy before 0.12.1 creates insecure temporary directories.
Severity: MEDIUM (CVSS v3 base score: 7.8)
Weakness: CWE-269 \u2014 Improper Privilege Management
The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.
Repository: https://github.com/scipy/scipy
Files changed:
scipy/weave/catalog.py
scipy/weave/tests/test_catalog.py
Commit message: BUG: weave: fix issues with temporary directory usage


Vulnerable code:
    # parallel.  We use both hostname and pocess id for the prefix in an
        in on you.


    if hasattr(default_dir,"cached_path") and \
       os.path.exists(default_dir.cached_path) and \
       os.access(default_dir.cached_path, os.W_OK):
    python_name = "python%d%d_compiled" % tuple(sys.version_info[:2])
    path_candidates = []
    if sys.platform != 'win32':
        try:
            path_candidates.append(os.path.join(os.environ['HOME'],
                                                '.' + python_name))
        except KeyError:
            pass

        temp_dir = repr(os.getuid()) + '_' + python_name
        path_candidates.append(os.path.join(tempfile.gettempdir(), temp_dir))
        path_candidates.append(os.path.join(tempfile.gettempdir(),
                                           "%s" % whoami(), python_name))

    writable = False
    for path in path_candidates:
        if not os.path.exists(path):
            try:
                os.makedirs(path, mode=0o700)
            except OSError:
                continue
        if is_writable(path):
            writable = True
            break

    if not writable:
        print('warning: default directory is not write accessible.')
        print('default:', path)
def intermediate_dir():
    """ Location in temp dir for storing .cpp and .o  files during
        builds.
    python_name = "python%d%d_intermediate" % tuple(sys.version_info[:2])
    path = os.path.join(tempfile.gettempdir(),"%s"%whoami(),python_name)
    if not os.path.exists(path):
        os.makedirs(path, mode=0o700)
    return path
    def remove_dir(self,d):
        import distutils.dir_util
        distutils.dir_util.remove_tree(d)

        self.remove_dir(pardir)
        self.remove_dir(pardir)

Fixed code:
import stat
    # parallel.  We use both hostname and process id for the prefix in an

def _create_dirs(path):
    """ create provided path, ignore errors """
    try:
        os.makedirs(path, mode=0o700)
    except OSError:
        pass


def default_dir_posix(tmp_dir=None):
    """
    Create or find default catalog store for posix systems

    purpose of 'tmp_dir' is to enable way how to test this function easily
    """
    path_candidates = []
    python_name = "python%d%d_compiled" % tuple(sys.version_info[:2])

    if tmp_dir:
        home_dir = tmp_dir
    else:
        home_dir = os.path.expanduser('~')
    tmp_dir = tmp_dir or tempfile.gettempdir()

    home_temp_dir_name = '.' + python_name
    home_temp_dir = os.path.join(home_dir, home_temp_dir_name)
    path_candidates.append(home_temp_dir)

    temp_dir_name = repr(os.getuid()) + '_' + python_name
    temp_dir_path = os.path.join(tmp_dir, temp_dir_name)
    path_candidates.append(temp_dir_path)

    for path in path_candidates:
        _create_dirs(path)
        if check_dir(path):
            return path

    # since we got here, both dirs are not useful
    tmp_dir_path = find_valid_temp_dir(temp_dir_name, tmp_dir)
    if not tmp_dir_path:
        tmp_dir_path = create_temp_dir(temp_dir_name, tmp_dir=tmp_dir)
    return tmp_dir_path


def default_dir_win(tmp_dir=None):
    """
    Create or find default catalog store for Windows systems

    purpose of 'tmp_dir' is to enable way how to test this function easily
    """
    def create_win_temp_dir(prefix, inner_dir=None, tmp_dir=None):
        """
        create temp dir starting with 'prefix' in 'tmp_dir' or
        'tempfile.gettempdir'; if 'inner_dir' is specified, it should be
        created inside
        """
        tmp_dir_path = find_valid_temp_dir(prefix, tmp_dir)
        if tmp_dir_path:
            if inner_dir:
                tmp_dir_path = os.path.join(tmp_dir_path, inner_dir)
                if not os.path.isdir(tmp_dir_path):
                    os.mkdir(tmp_dir_path, 0o700)
        else:
            tmp_dir_path = create_temp_dir(prefix, inner_dir, tmp_dir)
        return tmp_dir_path

    python_name = "python%d%d_compiled" % tuple(sys.version_info[:2])
    tmp_dir = tmp_dir or tempfile.gettempdir()

    temp_dir_name = "%s" % whoami()
    temp_root_dir = os.path.join(tmp_dir, temp_dir_name)
    temp_dir_path = os.path.join(temp_root_dir, python_name)
    _create_dirs(temp_dir_path)
    if check_dir(temp_dir_path) and check_dir(temp_root_dir):
        return temp_dir_path
    else:
        if check_dir(temp_root_dir):
            return create_win_temp_dir(python_name, tmp_dir=temp_root_dir)
        else:
            return create_win_temp_dir(temp_dir_name, python_name, tmp_dir)


        in on you. If the directory already exists in /tmp/ and is not
        secure, new one is created.
    if hasattr(default_dir, "cached_path") and \
       check_dir(default_dir.cached_path):
    if sys.platform == 'win32':
        path = default_dir_win()
        path = default_dir_posix()

def check_dir(im_dir):
    Check if dir is safe; if it is, return True.
    These checks make sense only on posix:
     * directory has correct owner
     * directory has correct permissions (0700)
     * directory is not a symlink
    """
    def check_is_dir():
        return os.path.isdir(im_dir)

    def check_permissions():
        """ If on posix, permissions should be 0700. """
        writable = is_writable(im_dir)
        if sys.platform != 'win32':
            try:
                im_dir_stat = os.stat(im_dir)
            except OSError:
                return False
            writable &= stat.S_IMODE(im_dir_stat.st_mode) == 0o0700
        return writable

    def check_ownership():
        """ Intermediate dir owner should be same as owner of process. """
        if sys.platform != 'win32':
            try:
                im_dir_stat = os.stat(im_dir)
            except OSError:
                return False
            proc_uid = os.getuid()
            return proc_uid == im_dir_stat.st_uid
        return True

    def check_is_symlink():
        """ Check if intermediate dir is symlink. """
        try:
            return not os.path.islink(im_dir)
        except OSError:
            return False

    checks = [check_is_dir, check_permissions,
              check_ownership, check_is_symlink]

    for check in checks:
        if not check():
            return False

    return True


def create_temp_dir(prefix, inner_dir=None, tmp_dir=None):
    """
    Create intermediate dirs <tmp>/<prefix+random suffix>/<inner_dir>/

    argument 'tmp_dir' is used in unit tests
    """
    if not tmp_dir:
        tmp_dir_path = tempfile.mkdtemp(prefix=prefix)
    else:
        tmp_dir_path = tempfile.mkdtemp(prefix=prefix, dir=tmp_dir)
    if inner_dir:
        tmp_dir_path = os.path.join(tmp_dir_path, inner_dir)
        os.mkdir(tmp_dir_path, 0o700)
    return tmp_dir_path


def intermediate_dir_prefix():
    """ Prefix of root intermediate dir (<tmp>/<root_im_dir>). """
    return "%s-%s-" % ("scipy", whoami())


def find_temp_dir(prefix, tmp_dir=None):
    """ Find temp dirs in 'tmp_dir' starting with 'prefix'"""
    matches = []
    tmp_dir = tmp_dir or tempfile.gettempdir()
    for tmp_file in os.listdir(tmp_dir):
        if tmp_file.startswith(prefix):
            matches.append(os.path.join(tmp_dir, tmp_file))
    return matches


def find_valid_temp_dir(prefix, tmp_dir=None):
    """
    Try to look for existing temp dirs.
    If there is one suitable found, return it, otherwise return None.
    """
    matches = find_temp_dir(prefix, tmp_dir)
    for match in matches:
        if check_dir(match):
            # as soon as we find correct dir, we can stop searching
            return match


def py_intermediate_dir():
    """
    Name of intermediate dir for current python interpreter:
    <temp dir>/<name>/pythonXY_intermediate/
    """
    name = "python%d%d_intermediate" % tuple(sys.version_info[:2])
    return name


def create_intermediate_dir(tmp_dir=None):
    py_im_dir = py_intermediate_dir()
    return create_temp_dir(intermediate_dir_prefix(), py_im_dir, tmp_dir)


def intermediate_dir(tmp_dir=None):
    """
    Temporary directory for storing .cpp and .o files during builds.

    First, try to find the dir and if it exists, verify it is safe.
    Otherwise, create it.
    """
    im_dir = find_valid_temp_dir(intermediate_dir_prefix(), tmp_dir)
    py_im_dir = py_intermediate_dir()
    if im_dir is None:
        py_im_dir = py_intermediate_dir()
        im_dir = create_intermediate_dir(tmp_dir)
    else:
        im_dir = os.path.join(im_dir, py_im_dir)
        if not os.path.isdir(im_dir):
            os.mkdir(im_dir, 0o700)
    return im_dir

import stat
import tempfile

from distutils.dir_util import remove_tree
from numpy.testing.noseclasses import KnownFailureTest
class TestIntermediateDir(TestCase):
    """
    Tests for intermediate dir (store of .cpp and .o during builds).
    These tests test whether intermediate dir is safe. If it's not,
    new one should be created.
    """
    def dirs_are_valid(self, wrong_dir, tmpdir):
        """ test if new dir is created and is consistent """
        new_im_dir = catalog.intermediate_dir(tmpdir)
        assert_(not os.path.samefile(new_im_dir, wrong_dir))
        new_im_dir2 = catalog.intermediate_dir(tmpdir)
        assert_(os.path.samefile(new_im_dir, new_im_dir2))

    def test_ownership(self):
        """ test if intermediate dir is owned by correct user """
        if sys.platform != 'win32':
            im_dir = catalog.intermediate_dir()
            im_dir_stat = os.stat(im_dir)
            proc_uid = os.getuid()
            assert_(proc_uid == im_dir_stat.st_uid)
            r_im_dir_stat = os.stat(os.path.dirname(im_dir))
            assert_(proc_uid == r_im_dir_stat.st_uid)

    def test_incorrect_ownership(self):
        """
        test if new intermediate dir is created when there is only one
        im dir owned by improper user
        """
        if sys.platform != 'win32':
            import pwd
            tmpdir = tempfile.mkdtemp()
            try:
                im_dir = catalog.create_intermediate_dir(tmpdir)
                root_im_dir = os.path.dirname(im_dir)
                nobody = pwd.getpwnam('nobody')[2]
                nobody_g = pwd.getpwnam('nobody')[3]
                try:
                    os.chown(root_im_dir, nobody, nobody_g)
                except OSError:
                    raise KnownFailureTest("Can't change owner.")
                else:
                    self.dirs_are_valid(im_dir, tmpdir)
            finally:
                remove_tree(tmpdir)

    def test_permissions(self):
        """ im dir should have permissions 0700 """
        if sys.platform != 'win32':
            im_dir = catalog.intermediate_dir()
            im_dir_stat = os.stat(im_dir)
            assert_(stat.S_IMODE(im_dir_stat.st_mode) == 0o0700)
            r_im_dir_stat = os.stat(os.path.dirname(im_dir))
            assert_(stat.S_IMODE(r_im_dir_stat.st_mode) == 0o0700)

    def test_incorrect_permissions(self):
        """
        if permissions on existing im dir are not correct,
        new one should be created
        """
        if sys.platform != 'win32':
            tmpdir = tempfile.mkdtemp()
            try:
                im_dir = catalog.create_intermediate_dir(tmpdir)
                root_im_dir = os.path.dirname(im_dir)
                try:
                    os.chmod(root_im_dir, 0o777)
                except OSError:
                    raise KnownFailureTest("Can't set file permissions.")
                else:
                    self.dirs_are_valid(im_dir, tmpdir)
            finally:
                remove_tree(tmpdir)

    def test_symlink(self):
        """ im dir shouldn't be a symlink """
        if sys.platform != 'win32':
            r_im_dir = os.path.dirname(catalog.intermediate_dir())
            assert_(os.path.islink(r_im_dir) is False)

    def test_symlink_raise(self):
        """ if existing im dir is a symlink, new one should be created """
        if sys.platform != 'win32':
            tmpdir = tempfile.mkdtemp()
            try:
                im_dir = catalog.create_intermediate_dir(tmpdir)
                root_im_dir = os.path.dirname(im_dir)

                tempdir = tempfile.mkdtemp(prefix='scipy-test', dir=tmpdir)
                try:
                    os.rename(root_im_dir, tempdir)
                except OSError:
                    raise KnownFailureTest("Can't move intermediate dir.")

                try:
                    os.symlink(tempdir, root_im_dir)
                except OSError:
                    raise KnownFailureTest(
                        "Can't create symlink to intermediate dir.")
                else:
                    self.dirs_are_valid(im_dir, tmpdir)
            finally:
                remove_tree(tmpdir)


    """
    Tests for 'catalog.default_dir()'.
    These should verified posix and win default_dir function.
    """
    def test_win(self):
        """
        test if default_dir for Windows platform is accessible

        since default_dir_win() does not have any Windows specific code,
        let's test it everywhere
        """
        d = catalog.default_dir_win()
        assert_(catalog.is_writable(d))

    def test_win_inaccessible_root(self):
        """
        there should be a new root dir created if existing one is not accessible
        """
        tmpdir = tempfile.mkdtemp()
        try:
            d_dir = catalog.default_dir_win(tmpdir)
            root_ddir = os.path.dirname(d_dir)

            try:
                os.chmod(root_ddir, stat.S_IREAD | stat.S_IEXEC)
            except OSError:
                raise KnownFailureTest("Can't change permissions of root default_dir.")

            new_ddir = catalog.default_dir_win(tmpdir)
            assert_(not os.path.samefile(new_ddir, d_dir))
            new_ddir2 = catalog.default_dir_win(tmpdir)
            assert_(os.path.samefile(new_ddir, new_ddir2))
        finally:
            os.chmod(root_ddir, 0o700)
            remove_tree(tmpdir)

    def test_win_inaccessible_ddir(self):
        """
        create new defualt_dir if current one is not accessible
        """
        tmpdir = tempfile.mkdtemp()
        try:
            d_dir = catalog.default_dir_win(tmpdir)

            try:
                os.chmod(d_dir, stat.S_IREAD | stat.S_IEXEC)
            except OSError:
                raise KnownFailureTest("Can't change permissions of default_dir.")

            new_ddir = catalog.default_dir_win(tmpdir)
            assert_(not os.path.samefile(new_ddir, d_dir))
            new_ddir2 = catalog.default_dir_win(tmpdir)
            assert_(os.path.samefile(new_ddir, new_ddir2))
        finally:
            os.chmod(d_dir, 0o700)
            remove_tree(tmpdir)

    def test_posix(self):
        """ test if posix default_dir is writable """
        d = catalog.default_dir_posix()
        assert_(catalog.is_writable(d))

    def test_posix_home_inaccessible(self):
        """ what happens when home catalog dir is innaccessible """
        tmpdir = tempfile.mkdtemp()
        try:
            d_dir = catalog.default_dir_posix(tmpdir)

            try:
                os.chmod(d_dir, 0o000)
            except OSError:
                raise KnownFailureTest("Can't change permissions of default_dir.")

            new_ddir = catalog.default_dir_posix(tmpdir)
            assert_(not os.path.samefile(new_ddir, d_dir))
            new_ddir2 = catalog.default_dir_posix(tmpdir)
            assert_(os.path.samefile(new_ddir, new_ddir2))
        finally:
            os.chmod(d_dir, 0o700)
            remove_tree(tmpdir)

    def test_posix_dirs_inaccessible(self):
        """ test if new dir is created if both implicit dirs are not valid"""
        tmpdir = tempfile.mkdtemp()
        try:
            d_dir = catalog.default_dir_posix(tmpdir)

            try:
                os.chmod(d_dir, 0o000)
            except OSError:
                raise KnownFailureTest("Can't change permissions of default_dir.")

            d_dir2 = catalog.default_dir_posix(tmpdir)

            try:
                os.chmod(d_dir2, 0o000)
            except OSError:
                raise KnownFailureTest("Can't change permissions of default_dir.")

            new_ddir = catalog.default_dir_posix(tmpdir)
            assert_(not (os.path.samefile(new_ddir, d_dir) or os.path.samefile(new_ddir, d_dir2)))
            new_ddir2 = catalog.default_dir_posix(tmpdir)
            assert_(os.path.samefile(new_ddir, new_ddir2))
        finally:
            os.chmod(d_dir, 0o700)
            os.chmod(d_dir2, 0o700)
            remove_tree(tmpdir)

        """ default_dir has to be writable """
        remove_tree(pardir)
        remove_tree(pardir)