]> granicus.if.org Git - python/commitdiff
#14984: On POSIX, enforce permissions when reading default .netrc.
authorR David Murray <rdmurray@bitdance.com>
Mon, 16 Sep 2013 17:48:44 +0000 (13:48 -0400)
committerR David Murray <rdmurray@bitdance.com>
Mon, 16 Sep 2013 17:48:44 +0000 (13:48 -0400)
Initial patch by Bruno Piguet.

This is implemented as if a useful .netrc file could exist without passwords,
which is possible in the general case; but in fact our netrc implementation
does not support it.  Fixing that issue will be an enhancement.

Doc/library/netrc.rst
Lib/netrc.py
Lib/test/test_netrc.py
Misc/NEWS

index 8a2f1c6088f1199bd7a81009632e5361cc43bf6f..97927998fabab95d33143f6169cb8f0eff71943a 100644 (file)
@@ -21,6 +21,12 @@ the Unix :program:`ftp` program and other FTP clients.
    no argument is given, the file :file:`.netrc` in the user's home directory will
    be read.  Parse errors will raise :exc:`NetrcParseError` with diagnostic
    information including the file name, line number, and terminating token.
+   If no argument is specified on a POSIX system, the presence of passwords in
+   the :file:`.netrc` file will raise a :exc:`NetrcParseError` if the file
+   ownership or permissions are insecure (owned by a user other than the user
+   running the process, or accessible for read or write by any other user).
+   This implements security behavior equivalent to that of ftp and other
+   programs that use :file:`.netrc`.
 
 
 .. exception:: NetrcParseError
index 5493d77d3e805da491b365dc8f1b246fe8d3eefe..0b4eedff4eed4aa00983c2a1ad5c6d4de640567c 100644 (file)
@@ -2,7 +2,7 @@
 
 # Module and documentation by Eric S. Raymond, 21 Dec 1998
 
-import os, shlex
+import os, stat, shlex, pwd
 
 __all__ = ["netrc", "NetrcParseError"]
 
@@ -21,6 +21,7 @@ class NetrcParseError(Exception):
 
 class netrc:
     def __init__(self, file=None):
+        default_netrc = file is None
         if file is None:
             try:
                 file = os.path.join(os.environ['HOME'], ".netrc")
@@ -77,6 +78,26 @@ class netrc:
                 elif tt == 'account':
                     account = lexer.get_token()
                 elif tt == 'password':
+                    if os.name == 'posix' and default_netrc:
+                        prop = os.fstat(fp.fileno())
+                        if prop.st_uid != os.getuid():
+                            try:
+                                fowner = pwd.getpwuid(prop.st_uid)[0]
+                            except KeyError:
+                                fowner = 'uid %s' % prop.st_uid
+                            try:
+                                user = pwd.getpwuid(os.getuid())[0]
+                            except KeyError:
+                                user = 'uid %s ' % os.getuid()
+                            raise NetrcParseError(
+                                ("~/.netrc file owner (%s) does not match"
+                                 " current user (%s)") % (fowner, user),
+                                file, lexer.lineno)
+                        if (prop.st_mode & (stat.S_IRWXG | stat.S_IRWXO)):
+                            raise NetrcParseError(
+                               "~/.netrc access too permissive: access"
+                               " permissions must restrict access to only"
+                               " the owner", file, lexer.lineno)
                     password = lexer.get_token()
                 else:
                     raise NetrcParseError("bad follower token %r" % tt,
index b536255aba3d36126159c06ffe94f0b64324bbfb..24ad78688c7032ac32c0cb5c4a79c42479bd7a4d 100644 (file)
@@ -32,7 +32,7 @@ class NetrcTestCase(unittest.TestCase):
 
     def tearDown (self):
         del self.netrc
-        os.unlink(temp_filename)
+        test_support.unlink(temp_filename)
 
     def test_case_1(self):
         self.assert_(self.netrc.macros == {'macro1':['line1\n', 'line2\n'],
@@ -41,6 +41,27 @@ class NetrcTestCase(unittest.TestCase):
         self.assert_(self.netrc.hosts['foo'] == ('log1', 'acct1', 'pass1'))
         self.assert_(self.netrc.hosts['default'] == ('log2', None, 'pass2'))
 
+    if os.name == 'posix':
+        def test_security(self):
+            # This test is incomplete since we are normally not run as root and
+            # therefore can't test the file ownership being wrong.
+            os.unlink(temp_filename)
+            d = test_support.TESTFN
+            try:
+                os.mkdir(d)
+                fn = os.path.join(d, '.netrc')
+                with open(fn, 'wt') as f:
+                    f.write(TEST_NETRC)
+                with test_support.EnvironmentVarGuard() as environ:
+                    environ.set('HOME', d)
+                    os.chmod(fn, 0600)
+                    self.netrc = netrc.netrc()
+                    self.test_case_1()
+                    os.chmod(fn, 0622)
+                    self.assertRaises(netrc.NetrcParseError, netrc.netrc)
+            finally:
+                test_support.rmtree(d)
+
 def test_main():
     test_support.run_unittest(NetrcTestCase)
 
index de8c2021f26270d37d8b0a7c1cdf88d0ff34d2fa..833cd05fa8200b3dff1c93bf2564af90e1333fbf 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -13,6 +13,12 @@ Core and Builtins
 Library
 -------
 
+- Issue #14984: On POSIX systems, when netrc is called without a filename
+  argument (and therefore is reading the user's $HOME/.netrc file), it now
+  enforces the same security rules as typical ftp clients: the .netrc file must
+  be owned by the user that owns the process and must not be readable by any
+  other user.
+
 - Issue #16248: Disable code execution from the user's home directory by
   tkinter when the -E flag is passed to Python.  Patch by Zachary Ware.