Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
gh-139633: Run netrc file permission check only once per parse
Change the `.netrc` security check to be run once per parse of the
default file rather than once per line inside the file.
  • Loading branch information
cmaloney committed Oct 5, 2025
commit e81151a4be545b4603875cf8ee7580a61cec0e18
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
self._security_check(fp)
break

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
26 changes: 26 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 Mock


temp_filename = os_helper.TESTFN

Expand Down Expand Up @@ -309,6 +312,29 @@ 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.
check_called = netrc.netrc._security_check = Mock(return_value=True)

# Parse a default netrc with more than one password line.
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()

check_called.assert_called_once()
del check_called


if __name__ == "__main__":
unittest.main()
Loading