Skip to content

Commit 1d82c81

Browse files
author
George Zefi
committed
Fix insecure file permissions when writing credentials in CodeArtifact login.py
1 parent 9f0a5be commit 1d82c81

3 files changed

Lines changed: 95 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: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,17 @@ def _update_netrc_entry(self, hostname, new_entry, netrc_path):
183183
if new_contents == contents:
184184
new_contents = self._append_netrc_entry(new_contents, new_entry)
185185

186-
with open(netrc_path, 'w') as f:
186+
fd = os.open(netrc_path,
187+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
188+
189+
try:
190+
os.chmod(fd, 0o600) # Ensure secure perms on pre-existing files
191+
except OSError as e:
192+
uni_print('Unable to set file permissions '
193+
'for %s: %s%s' % (netrc_path, e, os.linesep),
194+
sys.stderr)
195+
196+
with os.fdopen(fd, 'w') as f:
187197
f.write(new_contents)
188198

189199
def _create_netrc_file(self, netrc_path, new_entry):
@@ -631,7 +641,17 @@ def login(self, dry_run=False):
631641
sys.stdout.write(pypi_rc_str)
632642
sys.stdout.write(os.linesep)
633643
else:
634-
with open(self.pypi_rc_path, 'w+') as fp:
644+
fd = os.open(self.pypi_rc_path,
645+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
646+
647+
try:
648+
os.chmod(fd, 0o600) # Ensure secure perms on pre-existing files
649+
except OSError as e:
650+
uni_print('Unable to set file permissions '
651+
'for %s: %s%s' % (self.pypi_rc_path, e, os.linesep),
652+
sys.stderr)
653+
654+
with os.fdopen(fd, 'w') as fp:
635655
fp.write(pypi_rc_str)
636656

637657
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
@@ -435,6 +435,38 @@ def test_login_dry_run(self):
435435
self.subprocess_utils.check_output.assert_not_called()
436436
self.assertFalse(os.path.exists(self.test_netrc_path))
437437

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

439471
class TestNuGetLogin(unittest.TestCase):
440472
_NUGET_INDEX_URL_FMT = NuGetLogin._NUGET_INDEX_URL_FMT
@@ -1246,6 +1278,42 @@ def test_login_existing_pypi_rc_with_codeartifact_not_clobbered(self):
12461278
password='JgCXIr5xGG'
12471279
)
12481280

1281+
def test_login_sets_secure_permissions_on_new_file(self):
1282+
self.assertFalse(os.path.exists(self.test_pypi_rc_path))
1283+
self.test_subject.login()
1284+
file_mode = os.stat(self.test_pypi_rc_path).st_mode
1285+
self.assertEqual(stat.S_IMODE(file_mode), 0o600)
1286+
1287+
def test_login_adjusts_permissions_on_preexisting_file(self):
1288+
existing_pypi_rc = '''\
1289+
[distutils]
1290+
index-servers=
1291+
pypi
1292+
1293+
[pypi]
1294+
repository: http://www.python.org/pypi/
1295+
username: test
1296+
password: JgCXIr5xGG
1297+
'''
1298+
with open(self.test_pypi_rc_path, 'w') as f:
1299+
f.write(existing_pypi_rc)
1300+
os.chmod(self.test_pypi_rc_path, 0o644)
1301+
self.test_subject.login()
1302+
file_mode = os.stat(self.test_pypi_rc_path).st_mode
1303+
self.assertEqual(stat.S_IMODE(file_mode), 0o600)
1304+
1305+
@mock.patch('sys.stderr')
1306+
@mock.patch('os.chmod', side_effect=OSError(
1307+
errno.EPERM, 'Operation not permitted'
1308+
))
1309+
def test_login_writes_error_when_chmod_fails(self, mock_chmod, mock_stderr):
1310+
self.test_subject.login()
1311+
mock_stderr.write.assert_any_call(
1312+
'Unable to set file permissions for %s: '
1313+
'[Errno 1] Operation not permitted%s'
1314+
% (self.test_pypi_rc_path, os.linesep)
1315+
)
1316+
12491317
def test_login_existing_invalid_pypi_rc_error(self):
12501318
# This is an invalid pypirc as the list of servers are expected under
12511319
# an 'index-servers' option instead of 'servers'.

0 commit comments

Comments
 (0)