Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 22 additions & 17 deletions Lib/netrc.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,23 +152,28 @@ def _parse(self, file, fp, default_netrc):
else:
raise NetrcParseError("bad follower token %r" % tt,
file, lexer.lineno)
self._security_check(fp, default_netrc, self.hosts[entryname][0])

def _security_check(self, fp, default_netrc, login):
if _can_security_check() and default_netrc and login != "anonymous":
prop = os.fstat(fp.fileno())
current_user_id = os.getuid()
if prop.st_uid != current_user_id:
fowner = _getpwuid(prop.st_uid)
user = _getpwuid(current_user_id)
raise NetrcParseError(
f"~/.netrc file owner ({fowner}) does not match"
f" current user ({user})")
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")

if _can_security_check() and default_netrc:
for entry in self.hosts.values():
if entry[0] != "anonymous":
# Raises on security issue; once passed once can exit.
self._security_check(fp)
return

def _security_check(self, fp):
prop = os.fstat(fp.fileno())
current_user_id = os.getuid()
if prop.st_uid != current_user_id:
fowner = _getpwuid(prop.st_uid)
user = _getpwuid(current_user_id)
raise NetrcParseError(
f"~/.netrc file owner ({fowner}) does not match"
f" current user ({user})")
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")

def authenticators(self, host):
"""Return a (user, account, password) tuple for given host."""
Expand Down
23 changes: 23 additions & 0 deletions Lib/test/test_netrc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import netrc, os, unittest, sys, textwrap
from pathlib import Path
from test import support
from test.support import os_helper
from unittest.mock import patch


temp_filename = os_helper.TESTFN

Expand Down Expand Up @@ -309,6 +312,26 @@ def test_security(self):
self.assertEqual(nrc.hosts['foo.domain.com'],
('anonymous', '', 'pass'))

@unittest.skipUnless(os.name == 'posix', 'POSIX only test')
@unittest.skipUnless(hasattr(os, 'getuid'), "os.getuid is required")
@os_helper.skip_unless_working_chmod
def test_security_only_once(self):
# Make sure security check is only run once per parse when multiple
# entries are found.
with patch.object(netrc.netrc, "_security_check") as mock:
with os_helper.temp_dir() as tmp_dir:
netrc_path = Path(tmp_dir) / '.netrc'
netrc_path.write_text("""\
machine foo.domain.com login bar password pass
machine bar.domain.com login foo password pass
""")
netrc_path.chmod(0o600)
with os_helper.EnvironmentVarGuard() as environ:
environ.set('HOME', tmp_dir)
netrc.netrc()

mock.assert_called_once()


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The :mod:`netrc` security check is now run once per parse rather than once
per entry.
Loading