103 lines
4.1 KiB
Diff
103 lines
4.1 KiB
Diff
Index: paramiko-2.4.2/paramiko/pkey.py
|
|
===================================================================
|
|
--- paramiko-2.4.2.orig/paramiko/pkey.py
|
|
+++ paramiko-2.4.2/paramiko/pkey.py
|
|
@@ -519,7 +519,18 @@ class PKey(object):
|
|
|
|
:raises: ``IOError`` -- if there was an error writing the file.
|
|
"""
|
|
- with open(filename, "w") as f:
|
|
+ # Ensure that we create new key files directly with a user-only mode,
|
|
+ # instead of opening, writing, then chmodding, which leaves us open to
|
|
+ # CVE-2022-24302.
|
|
+ # NOTE: O_TRUNC is a noop on new files, and O_CREAT is a noop on
|
|
+ # existing files, so using all 3 in both cases is fine. Ditto the use
|
|
+ # of the 'mode' argument; it should be safe to give even for existing
|
|
+ # files (though it will not act like a chmod in that case).
|
|
+ # TODO 3.0: turn into kwargs again
|
|
+ args = [os.O_WRONLY | os.O_TRUNC | os.O_CREAT, o600]
|
|
+ # NOTE: yea, you still gotta inform the FLO that it is in "write" mode
|
|
+ with os.fdopen(os.open(filename, *args), "w") as f:
|
|
+ # TODO 3.0: remove the now redundant chmod
|
|
os.chmod(filename, o600)
|
|
self._write_private_key(f, key, format, password=password)
|
|
|
|
Index: paramiko-2.4.2/tests/test_pkey.py
|
|
===================================================================
|
|
--- paramiko-2.4.2.orig/tests/test_pkey.py
|
|
+++ paramiko-2.4.2/tests/test_pkey.py
|
|
@@ -23,11 +23,15 @@ Some unit tests for public/private key o
|
|
|
|
import unittest
|
|
import os
|
|
+import stat
|
|
from binascii import hexlify
|
|
from hashlib import md5
|
|
|
|
from paramiko import RSAKey, DSSKey, ECDSAKey, Ed25519Key, Message, util
|
|
from paramiko.py3compat import StringIO, byte_chr, b, bytes, PY2
|
|
+from paramiko.common import o600
|
|
+
|
|
+from mock import patch, Mock
|
|
|
|
from .util import _support
|
|
|
|
@@ -567,3 +571,57 @@ class KeyTest(unittest.TestCase):
|
|
key1.load_certificate,
|
|
_support("test_rsa.key-cert.pub"),
|
|
)
|
|
+
|
|
+ @patch("paramiko.pkey.os")
|
|
+ def _test_keyfile_race(self, os_, exists):
|
|
+ # Re: CVE-2022-24302
|
|
+ password = "television"
|
|
+ newpassword = "radio"
|
|
+ source = _support("test_ecdsa_384.key")
|
|
+ new = source + ".new"
|
|
+ # Mock setup
|
|
+ os_.path.exists.return_value = exists
|
|
+ # Attach os flag values to mock
|
|
+ for attr, value in vars(os).items():
|
|
+ if attr.startswith("O_"):
|
|
+ setattr(os_, attr, value)
|
|
+ # Load fixture key
|
|
+ key = ECDSAKey(filename=source, password=password)
|
|
+ key._write_private_key = Mock()
|
|
+ # Write out in new location
|
|
+ key.write_private_key_file(new, password=newpassword)
|
|
+ # Expected open via os module
|
|
+ os_.open.assert_called_once_with(new, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, o600)
|
|
+ os_.fdopen.assert_called_once_with(os_.open.return_value, "w")
|
|
+ # Old chmod still around for backwards compat
|
|
+ os_.chmod.assert_called_once_with(new, o600)
|
|
+ assert (
|
|
+ key._write_private_key.call_args[0][0]
|
|
+ == os_.fdopen.return_value.__enter__.return_value
|
|
+ )
|
|
+
|
|
+ def test_new_keyfiles_avoid_file_descriptor_race_on_chmod(self):
|
|
+ self._test_keyfile_race(exists=False)
|
|
+
|
|
+ def test_existing_keyfiles_still_work_ok(self):
|
|
+ self._test_keyfile_race(exists=True)
|
|
+
|
|
+ def test_new_keyfiles_avoid_descriptor_race_integration(self):
|
|
+ # Integration-style version of above
|
|
+ password = "television"
|
|
+ newpassword = "radio"
|
|
+ source = _support("test_ecdsa_384.key")
|
|
+ new = source + ".new"
|
|
+ # Load fixture key
|
|
+ key = ECDSAKey(filename=source, password=password)
|
|
+ try:
|
|
+ # Write out in new location
|
|
+ key.write_private_key_file(new, password=newpassword)
|
|
+ # Test mode
|
|
+ assert stat.S_IMODE(os.stat(new).st_mode) == o600
|
|
+ # Prove can open with new password
|
|
+ reloaded = ECDSAKey(filename=new, password=newpassword)
|
|
+ assert reloaded == key
|
|
+ finally:
|
|
+ if os.path.exists(new):
|
|
+ os.unlink(new)
|