diff --git a/AUTHORS.txt b/AUTHORS.txt index fcd1240dd02..b5b3d61d1e7 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -14,6 +14,7 @@ Clay McClure Cody Soyland Daniel Holth Dave Abrahams +David (d1b) Dmitry Gladkov Donald Stufft Francesco diff --git a/CHANGES.txt b/CHANGES.txt index 773cc662bb8..91e7c29cf6f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -7,6 +7,9 @@ develop (unreleased) * Added "pip list" for listing installed packages and the latest version available. Thanks Rafael Caricio, Miguel Araujo, Dmitry Gladkov (Pull #752) +* Fixed security issues with pip's use of temp build directories. + Thanks David (d1b) and Thomas Güttler. (Pull #780) + * Improvements to sphinx docs and cli help. (Pull #773) * Fixed issue #707, dealing with OS X temp dir handling, which was causing diff --git a/docs/cookbook.txt b/docs/cookbook.txt index 1b9a9c17e30..777b39e9860 100644 --- a/docs/cookbook.txt +++ b/docs/cookbook.txt @@ -72,7 +72,7 @@ pip allows you to *just* unpack archives to a build directory without installing $ pip install --no-install SomePackage -If you're in a virtualenv, the build dir is ``/build``. Otherwise, it's ``/pip-build`` +If you're in a virtualenv, the build dir is ``/build``. Otherwise, it's ``/pip-build-`` Afterwards, to finish the job of installing unpacked archives, run:: diff --git a/pip/commands/install.py b/pip/commands/install.py index 61b1541d6ce..959e8900f92 100644 --- 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 "/build". ' - 'The default for global installs is "/pip-build".') + 'The default for global installs is "/pip-build-".') cmd_opts.add_option( '-t', '--target', diff --git a/pip/locations.py b/pip/locations.py index d378dd73997..36efdcaa9e5 100644 --- 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 index cfeaccbbbc7..27692b3787a 100644 --- 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) diff --git a/tests/test_locations.py b/tests/test_locations.py new file mode 100644 index 00000000000..0db0f972a6e --- /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()