Skip to content

Commit 3064dde

Browse files
author
George Zefi
committed
Fix insecure file permissions when writing credentials in CodeArtifact login.py
1 parent e12e464 commit 3064dde

3 files changed

Lines changed: 96 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "bugfix",
3+
"category": "CodeArtifact",
4+
"description": "Tighten file permissions when writing credentials in CodeArtifact login"
5+
}

awscli/customizations/codeartifact/login.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,17 @@ def _update_netrc_entry(self, hostname, new_entry, netrc_path):
191191
new_contents, new_entry
192192
)
193193

194-
with open(netrc_path, 'w') as f:
194+
fd = os.open(netrc_path,
195+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
196+
197+
try:
198+
os.chmod(fd, 0o600) # Ensure secure perms on pre-existing files
199+
except OSError as e:
200+
uni_print('Unable to set file permissions '
201+
'for %s: %s%s' % (netrc_path, e, os.linesep),
202+
sys.stderr)
203+
204+
with os.fdopen(fd, 'w') as f:
195205
f.write(new_contents)
196206

197207
def _create_netrc_file(self, netrc_path, new_entry):
@@ -635,7 +645,18 @@ def login(self, dry_run=False):
635645
sys.stdout.write(pypi_rc_str)
636646
sys.stdout.write(os.linesep)
637647
else:
638-
with open(self.pypi_rc_path, 'w+') as fp:
648+
else:
649+
fd = os.open(self.pypi_rc_path,
650+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
651+
652+
try:
653+
os.chmod(fd, 0o600) # Ensure secure perms on pre-existing files
654+
except OSError as e:
655+
uni_print('Unable to set file permissions '
656+
'for %s: %s%s' % (self.pypi_rc_path, e, os.linesep),
657+
sys.stderr)
658+
659+
with os.fdopen(fd, 'w') as fp:
639660
fp.write(pypi_rc_str)
640661

641662
self._write_success_message('twine')

tests/unit/customizations/codeartifact/test_adapter_login.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,37 @@ def test_login_dry_run(self):
425425
self.subprocess_utils.check_output.assert_not_called()
426426
self.assertFalse(os.path.exists(self.test_netrc_path))
427427

428+
@mock.patch('awscli.customizations.codeartifact.login.is_macos', False)
429+
def test_login_adjusts_permissions_on_preexisting_file(self):
430+
existing_content = (
431+
f'machine {self.hostname} login token password old-token\n'
432+
)
433+
with open(self.test_netrc_path, 'w') as f:
434+
f.write(existing_content)
435+
os.chmod(self.test_netrc_path, 0o644)
436+
self.test_subject.login()
437+
file_mode = os.stat(self.test_netrc_path).st_mode
438+
self.assertEqual(stat.S_IMODE(file_mode), 0o600)
439+
440+
@mock.patch('awscli.customizations.codeartifact.login.is_macos', False)
441+
@mock.patch('sys.stderr')
442+
@mock.patch('os.chmod', side_effect=OSError(
443+
errno.EPERM, 'Operation not permitted'
444+
))
445+
def test_login_writes_error_when_chmod_fails(
446+
self, mock_chmod, mock_stderr
447+
):
448+
existing_content = (
449+
f'machine {self.hostname} login token password old-token\n'
450+
)
451+
with open(self.test_netrc_path, 'w') as f:
452+
f.write(existing_content)
453+
self.test_subject.login()
454+
mock_stderr.write.assert_any_call(
455+
'Unable to set file permissions for %s: '
456+
'[Errno 1] Operation not permitted%s'
457+
% (self.test_netrc_path, os.linesep)
458+
)
428459

429460
class TestNuGetLogin(unittest.TestCase):
430461
_NUGET_INDEX_URL_FMT = NuGetLogin._NUGET_INDEX_URL_FMT
@@ -1316,6 +1347,43 @@ def test_login_existing_pypi_rc_with_codeartifact_not_clobbered(self):
13161347
password='JgCXIr5xGG',
13171348
)
13181349

1350+
def test_login_sets_secure_permissions_on_new_file(self):
1351+
self.assertFalse(os.path.exists(self.test_pypi_rc_path))
1352+
self.test_subject.login()
1353+
file_mode = os.stat(self.test_pypi_rc_path).st_mode
1354+
self.assertEqual(stat.S_IMODE(file_mode), 0o600)
1355+
1356+
def test_login_adjusts_permissions_on_preexisting_file(self):
1357+
existing_pypi_rc = '''\
1358+
[distutils]
1359+
index-servers=
1360+
pypi
1361+
1362+
[pypi]
1363+
repository: http://www.python.org/pypi/
1364+
username: test
1365+
password: JgCXIr5xGG
1366+
'''
1367+
with open(self.test_pypi_rc_path, 'w') as f:
1368+
f.write(existing_pypi_rc)
1369+
os.chmod(self.test_pypi_rc_path, 0o644)
1370+
self.test_subject.login()
1371+
file_mode = os.stat(self.test_pypi_rc_path).st_mode
1372+
self.assertEqual(stat.S_IMODE(file_mode), 0o600)
1373+
1374+
@mock.patch('sys.stderr')
1375+
@mock.patch('os.chmod', side_effect=OSError(
1376+
errno.EPERM, 'Operation not permitted'
1377+
))
1378+
1379+
def test_login_writes_error_when_chmod_fails(self, mock_chmod, mock_stderr):
1380+
self.test_subject.login()
1381+
mock_stderr.write.assert_any_call(
1382+
'Unable to set file permissions for %s: '
1383+
'[Errno 1] Operation not permitted%s'
1384+
% (self.test_pypi_rc_path, os.linesep)
1385+
)
1386+
13191387
def test_login_existing_invalid_pypi_rc_error(self):
13201388
# This is an invalid pypirc as the list of servers are expected under
13211389
# an 'index-servers' option instead of 'servers'.

0 commit comments

Comments
 (0)