Repository: pypa/pip

Issue:
/tmp/pip-build not secure
Well known temporary file names like /tmp/pip-build are insecure.

eviluser@host:~$ ln -s /home/otheruser/some-directory /tmp/pip-build

otheruser@host:~$ pip install ....

--> pip writes in  /home/otheruser/some-directory.  The user "otheruser" does not know it.

I tried it with pip 1.2.1 and content was written to /home/otheruser/some-directory.



Discussion:
Can you check develop branch of pip I believe pull #516 would have fixed this.

/tmp/pip-build is still used and can be abused by the way I posted above.

I tried it just some minutes ago with current git branch develop.

You can see it here, too:

strace -f  pip install --user foo 2> tmp/strace-pip.log


Resolving patch:
diff --git a/pip/commands/install.py b/pip/commands/install.py
--- a/pip/commands/install.py
+++ b/pip/commands/install.py
@@ -70,7 +70,7 @@ def __init__(self, *args, **kw):
             default=build_prefix,
             help='Directory to unpack packages into and build in. '
             'The default in a virtualenv is "<venv path>/build". '
-            'The default for global installs is "<OS temp dir>/pip-build".')
+            'The default for global installs is "<OS temp dir>/pip-build-<username>".')
 
         cmd_opts.add_option(
             '-t', '--target',
diff --git a/pip/locations.py b/pip/locations.py
--- a/pip/locations.py
+++ b/pip/locations.py
@@ -4,7 +4,9 @@
 import site
 import os
 import tempfile
+import getpass
 from pip.backwardcompat import get_python_lib
+import pip.exceptions
 
 
 def running_under_virtualenv():
@@ -25,6 +27,31 @@ def virtualenv_no_global():
     if running_under_virtualenv() and os.path.isfile(no_global_file):
         return True
 
+def _get_build_prefix():
+    """ Returns a safe build_prefix """
+    path = os.path.join(tempfile.gettempdir(), 'pip-build-%s' % \
+        getpass.getuser())
+    if sys.platform == 'win32':
+        """ on windows(tested on 7) temp dirs are isolated """
+        return path
+    try:
+        os.mkdir(path)
+    except OSError:
+        file_uid = None
+        try:
+            fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
+            file_uid = os.fstat(fd).st_uid
+            os.close(fd)
+        except OSError:
+            file_uid = None
+        if file_uid != os.getuid():
+            msg = "The temporary folder for building (%s) is not owned by your user!" \
+                % path
+            print (msg)
+            print("pip will not work until the temporary folder is " + \
+                 "either deleted or owned by your user account.")
+            raise pip.exceptions.InstallationError(msg)
+    return path
 
 if running_under_virtualenv():
     build_prefix = os.path.join(sys.prefix, 'build')
@@ -33,7 +60,7 @@ def virtualenv_no_global():
     # Use tempfile to create a temporary folder for build
     # Note: we are NOT using mkdtemp so we can have a consistent build dir
     # Note: using realpath due to tmp dirs on OSX being symlinks
-    build_prefix = os.path.realpath(os.path.join(tempfile.gettempdir(), 'pip-build'))
+    build_prefix = os.path.realpath(_get_build_prefix())
 
     ## FIXME: keep src in cwd for now (it is not a temporary folder)
     try:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -8,7 +8,7 @@
 here = os.path.abspath(os.path.dirname(__file__))
 
 def read(*parts):
-    return codecs.open(os.path.join(here, *parts), 'r', 'utf8').read()
+    return codecs.open(os.path.join(here, *parts), 'r').read()
 
 def find_version(*file_paths):
     version_file = read(*file_paths)


Test patch:
diff --git a/tests/test_locations.py b/tests/test_locations.py
new file mode 100644
--- /dev/null
+++ b/tests/test_locations.py
@@ -0,0 +1,102 @@
+"""
+locations.py tests
+
+"""
+import os
+import sys
+import shutil
+import tempfile
+import getpass
+from mock import Mock
+from nose import SkipTest
+from nose.tools import assert_raises
+import pip
+
+class TestLocations:
+    def setup(self):
+        self.tempdir = tempfile.mkdtemp()
+        self.st_uid = 9999
+        self.username = "example"
+        self.patch()
+
+    def tearDown(self):
+        self.revert_patch()
+        shutil.rmtree(self.tempdir, ignore_errors=True)
+
+    def patch(self):
+        """ first store and then patch python methods pythons """
+        self.tempfile_gettempdir = tempfile.gettempdir
+        self.old_os_fstat = os.fstat
+        if sys.platform != 'win32':
+            # os.getuid not implemented on windows
+            self.old_os_getuid = os.getuid
+        self.old_getpass_getuser = getpass.getuser
+
+        # now patch
+        tempfile.gettempdir = lambda : self.tempdir
+        getpass.getuser = lambda : self.username
+        os.getuid = lambda : self.st_uid
+        os.fstat = lambda fd : self.get_mock_fstat(fd)
+
+    def revert_patch(self):
+        """ revert the patches to python methods """
+        tempfile.gettempdir = self.tempfile_gettempdir
+        getpass.getuser = self.old_getpass_getuser
+        if sys.platform != 'win32':
+            # os.getuid not implemented on windows
+            os.getuid = self.old_os_getuid
+        os.fstat = self.old_os_fstat
+
+    def get_mock_fstat(self, fd):
+        """ returns a basic mock fstat call result.
+            Currently only the st_uid attribute has been set.
+        """
+        result = Mock()
+        result.st_uid = self.st_uid
+        return result
+
+    def get_build_dir_location(self):
+        """ returns a string pointing to the
+            current build_prefix.
+        """
+        return os.path.join(self.tempdir, 'pip-build-%s' % self.username)
+
+    def test_dir_path(self):
+        """ test the path name for the build_prefix
+        """
+        from pip import locations
+        assert locations._get_build_prefix() == self.get_build_dir_location()
+
+    def test_dir_created(self):
+        """ test that the build_prefix directory is generated when
+            _get_build_prefix is called.
+        """
+        #skip on windows, build dir is not created
+        if sys.platform == 'win32':
+            raise SkipTest()
+        assert not os.path.exists(self.get_build_dir_location() ), \
+            "the build_prefix directory should not exist yet!"
+        from pip import locations
+        locations._get_build_prefix()
+        assert os.path.exists(self.get_build_dir_location() ), \
+            "the build_prefix directory should now exist!"
+
+    def test_error_raised_when_owned_by_another(self):
+        """ test calling _get_build_prefix when there is a temporary
+            directory owned by another user raises an InstallationError.
+        """
+        #skip on windows; this exception logic only runs on linux
+        if sys.platform == 'win32':
+            raise SkipTest()
+        from pip import locations
+        os.getuid = lambda : 1111
+        os.mkdir(self.get_build_dir_location() )
+        assert_raises(pip.exceptions.InstallationError, locations._get_build_prefix)
+
+    def test_no_error_raised_when_owned_by_you(self):
+        """ test calling _get_build_prefix when there is a temporary
+            directory owned by you raise no InstallationError.
+        """
+        from pip import locations
+        os.mkdir(self.get_build_dir_location())
+        locations._get_build_prefix()