Merge #14984: On POSIX, enforce permissions when reading default .netrc.

This commit is contained in:
R David Murray 2013-09-17 21:28:17 -04:00
commit 4750fa8369
4 changed files with 61 additions and 6 deletions

View File

@ -22,6 +22,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 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 be read. Parse errors will raise :exc:`NetrcParseError` with diagnostic
information including the file name, line number, and terminating token. 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.4 Added the POSIX permission check.
.. exception:: NetrcParseError .. exception:: NetrcParseError

View File

@ -2,7 +2,7 @@
# Module and documentation by Eric S. Raymond, 21 Dec 1998 # Module and documentation by Eric S. Raymond, 21 Dec 1998
import io, os, shlex import io, os, shlex, stat, pwd
__all__ = ["netrc", "NetrcParseError"] __all__ = ["netrc", "NetrcParseError"]
@ -21,6 +21,7 @@ class NetrcParseError(Exception):
class netrc: class netrc:
def __init__(self, file=None): def __init__(self, file=None):
default_netrc = file is None
if file is None: if file is None:
try: try:
file = os.path.join(os.environ['HOME'], ".netrc") file = os.path.join(os.environ['HOME'], ".netrc")
@ -29,9 +30,9 @@ class netrc:
self.hosts = {} self.hosts = {}
self.macros = {} self.macros = {}
with open(file) as fp: 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 = shlex.shlex(fp)
lexer.wordchars += r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~""" lexer.wordchars += r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
lexer.commenters = lexer.commenters.replace('#', '') lexer.commenters = lexer.commenters.replace('#', '')
@ -86,6 +87,26 @@ class netrc:
elif tt == 'account': elif tt == 'account':
account = lexer.get_token() account = lexer.get_token()
elif tt == 'password': 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() password = lexer.get_token()
else: else:
raise NetrcParseError("bad follower token %r" % tt, raise NetrcParseError("bad follower token %r" % tt,

View File

@ -5,9 +5,6 @@ temp_filename = support.TESTFN
class NetrcTestCase(unittest.TestCase): class NetrcTestCase(unittest.TestCase):
def tearDown(self):
os.unlink(temp_filename)
def make_nrc(self, test_data): def make_nrc(self, test_data):
test_data = textwrap.dedent(test_data) test_data = textwrap.dedent(test_data)
mode = 'w' mode = 'w'
@ -15,6 +12,7 @@ class NetrcTestCase(unittest.TestCase):
mode += 't' mode += 't'
with open(temp_filename, mode) as fp: with open(temp_filename, mode) as fp:
fp.write(test_data) fp.write(test_data)
self.addCleanup(os.unlink, temp_filename)
return netrc.netrc(temp_filename) return netrc.netrc(temp_filename)
def test_default(self): def test_default(self):
@ -103,6 +101,28 @@ class NetrcTestCase(unittest.TestCase):
""", '#pass') """, '#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(): def test_main():
support.run_unittest(NetrcTestCase) support.run_unittest(NetrcTestCase)

View File

@ -12,6 +12,12 @@ Core and Builtins
Library 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 #18873: The tokenize module now detects Python source code encoding - Issue #18873: The tokenize module now detects Python source code encoding
only in comment lines. only in comment lines.