summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/netrc.rst8
-rw-r--r--Lib/netrc.py27
-rw-r--r--Lib/test/test_netrc.py26
-rw-r--r--Misc/NEWS6
4 files changed, 61 insertions, 6 deletions
diff --git a/Doc/library/netrc.rst b/Doc/library/netrc.rst
index 91990df..b6c1066 100644
--- a/Doc/library/netrc.rst
+++ b/Doc/library/netrc.rst
@@ -19,6 +19,14 @@ 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`.
+
+ .. versionchanged:: 3.1.6 Added the POSIX permission check.
.. exception:: NetrcParseError
diff --git a/Lib/netrc.py b/Lib/netrc.py
index c96db6f..8fe4cee 100644
--- a/Lib/netrc.py
+++ b/Lib/netrc.py
@@ -2,7 +2,7 @@
# Module and documentation by Eric S. Raymond, 21 Dec 1998
-import io, os, shlex
+import io, os, shlex, stat, 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")
@@ -29,9 +30,9 @@ class netrc:
self.hosts = {}
self.macros = {}
with open(file) as fp:
- self._parse(file, fp)
+ self._parse(file, fp, default_netrc)
- def _parse(self, file, fp):
+ def _parse(self, file, fp, default_netrc):
lexer = shlex.shlex(fp)
lexer.wordchars += r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
lexer.commenters = lexer.commenters.replace('#', '')
@@ -86,6 +87,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,
diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py
index ef70e37..60a3ec9 100644
--- a/Lib/test/test_netrc.py
+++ b/Lib/test/test_netrc.py
@@ -5,9 +5,6 @@ temp_filename = support.TESTFN
class NetrcTestCase(unittest.TestCase):
- def tearDown(self):
- os.unlink(temp_filename)
-
def make_nrc(self, test_data):
test_data = textwrap.dedent(test_data)
mode = 'w'
@@ -15,6 +12,7 @@ class NetrcTestCase(unittest.TestCase):
mode += 't'
with open(temp_filename, mode) as fp:
fp.write(test_data)
+ self.addCleanup(os.unlink, temp_filename)
return netrc.netrc(temp_filename)
def test_default(self):
@@ -103,6 +101,28 @@ class NetrcTestCase(unittest.TestCase):
""", '#pass')
+ @unittest.skipUnless(os.name == 'posix', 'POSIX only test')
+ 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.
+ d = support.TESTFN
+ os.mkdir(d)
+ self.addCleanup(support.rmtree, d)
+ fn = os.path.join(d, '.netrc')
+ with open(fn, 'wt') as f:
+ f.write("""\
+ machine foo.domain.com login bar password pass
+ default login foo password pass
+ """)
+ with support.EnvironmentVarGuard() as environ:
+ environ.set('HOME', d)
+ os.chmod(fn, 0o600)
+ nrc = netrc.netrc()
+ self.assertEqual(nrc.hosts['foo.domain.com'],
+ ('bar', None, 'pass'))
+ os.chmod(fn, 0o622)
+ self.assertRaises(netrc.NetrcParseError, netrc.netrc)
+
def test_main():
support.run_unittest(NetrcTestCase)
diff --git a/Misc/NEWS b/Misc/NEWS
index 0b5fab4..89ee27d 100644
--- 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.