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/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:


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,81 @@
+"""
+locations.py tests
+
+"""
+import os
+import sys
+import shutil
+import tempfile
+import getpass
+from mock import Mock
+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
+        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
+        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_created(self):
+        """ test that the build_prefix directory is generated when
+            _get_build_prefix is called.
+        """
+
+        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.
+        """
+        from pip import locations
+        os.getuid = lambda : 1111
+        os.mkdir(self.get_build_dir_location() )
+        try:
+            locations._get_build_prefix()
+            raise AssertionError("An InstallationError should have been raised!")
+        except pip.exceptions.InstallationError:
+            pass