CVE-2022-31124 (published 2022-07-06T18:15Z)

openssh_key_parser is an open source Python package providing utilities to parse and pack OpenSSH private and public key files. In versions prior to 0.0.6 if a field of a key is shorter than it is declared to be, the parser raises an error with a message containing the raw field value. An attacker able to modify the declared length of a key's sensitive field can thus expose the raw value of that field. Users are advised to upgrade to version 0.0.6, which no longer includes the raw field value in the error message. There are no known workarounds for this issue.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:P/I:N/A:N)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-209
cwe_name: Generation of Error Message Containing Sensitive Information
description: The product generates an error message that includes sensitive information about its environment, users, or associated data.
extended_description: The sensitive information may be valuable information on its own (such as a password), or it may be useful for launching other, more serious attacks. The error message may be created in different ways:
           
             self-generated: the source code explicitly constructs the error message and delivers it
	     externally-generated: the external environment, such as a language interpreter, handles the error and constructs its own message, whose contents are not under direct control by the programmer
	   
	 An attacker may use the contents of error messages to help launch another, more focused attack. For example, an attempt to exploit a path traversal weakness (CWE-22) might yield the full pathname of the installed application. In turn, this could be used to select the proper number of ".." sequences to navigate to the targeted file. An attack using SQL injection (CWE-89) might not initially succeed, but an error message could reveal the malformed query, which would expose query logic and possibly even passwords or other sensitive information used within the query.
         

Fix information:
repo_url: https://github.com/scottcwang/openssh_key_parser
commit_details: repo_url: https://github.com/scottcwang/openssh_key_parser
author: Michael Doyle
author_date: 2022-06-22 10:59:49-04:00
msg: Improved error handling to prevent unhandled exceptions in calling code.
repository: repo_url: https://github.com/scottcwang/openssh_key_parser
repo_name: scottcwang/openssh_key_parser
repo_language: Python
owner: scottcwang
file_changes: filename: private_key_list.py
change_type: ModificationType.MODIFY
diff: @@ -221,7 +221,9 @@ class PrivateKeyList(BaseList):
 
         Raises:
             ValueError: The provided byte string is not an ``openssh-key-v1``
-                key list or the declared key count is negative.
+                key list, when the declared key count is negative, or when an
+                EOF is found while parsing the key.
+
             UserWarning: The check numbers in the decrypted private byte string
                 do not match (likely due to an incorrect passphrase), the key
                 type or parameter values of a private key do not match that of
@@ -229,110 +231,117 @@ class PrivateKeyList(BaseList):
                 at the end of the decrypted private byte string are not as
                 expected.
         """
-        byte_stream = PascalStyleByteStream(byte_string)
+        try:
+            byte_stream = PascalStyleByteStream(byte_string)
 
-        header = byte_stream.read_from_format_instructions_dict(
-            cls.HEADER_FORMAT_INSTRUCTIONS_DICT
-        )
+            header = byte_stream.read_from_format_instructions_dict(
+                cls.HEADER_FORMAT_INSTRUCTIONS_DICT
+            )
+
+            if header['auth_magic'] != b'openssh-key-v1\x00':
+                raise ValueError('Not an openssh-key-v1 key')
 
-        if header['auth_magic'] != b'openssh-key-v1\x00':
-            raise ValueError('Not an openssh-key-v1 key')
+            num_keys = header['num_keys']
 
-        num_keys = header['num_keys']
+            if num_keys < 0:
+                raise ValueError('Cannot parse negative number of keys')
 
-        if num_keys < 0:
-            raise ValueError('Cannot parse negative number of keys')
+            public_key_list = []
+            for i in range(num_keys):
+                public_key_bytes = byte_stream.read_from_format_instruction(
+                    PascalStyleFormatInstruction.BYTES
+                )
+                public_key_list.append(
+                    PublicKey.from_bytes(public_key_bytes)
+                )
 
-        public_key_list = []
-        for i in range(num_keys):
-            public_key_bytes = byte_stream.read_from_format_instruction(
+            cipher_bytes = byte_stream.read_from_format_instruction(
                 PascalStyleFormatInstruction.BYTES
             )
-            public_key_list.append(
-                PublicKey.from_bytes(public_key_bytes)
-            )
 
-        cipher_bytes = byte_stream.read_from_format_instruction(
-            PascalStyleFormatInstruction.BYTES
-        )
-
-        kdf_class = get_kdf_options_class(header['kdf'])
-        kdf_options = kdf_class(
-            PascalStyleByteStream(
-                header['kdf_options']
-            ).read_from_format_instructions_dict(
-                kdf_class.FORMAT_INSTRUCTIONS_DICT
+            kdf_class = get_kdf_options_class(header['kdf'])
+            kdf_options = kdf_class(
+                PascalStyleByteStream(
+                    header['kdf_options']
+                ).read_from_format_instructions_dict(
+                    kdf_class.FORMAT_INSTRUCTIONS_DICT
+                )
             )
-        )
 
-        cipher_class = get_cipher_class(header['cipher'])
+            cipher_class = get_cipher_class(header['cipher'])
 
-        if kdf_class == NoneKDFOptions:
-            passphrase = ''
-        elif passphrase is None:
-            passphrase = getpass.getpass('Key passphrase: ')
+            if kdf_class == NoneKDFOptions:
+                passphrase = ''
+            elif passphrase is None:
+                passphrase = getpass.getpass('Key passphrase: ')
 
-        if issubclass(cipher_class, ConfidentialityIntegrityCipher):
-            cipher_bytes += byte_stream.read_fixed_bytes(
-                cipher_class.TAG_LENGTH
-            )
-
-        decipher_bytes = cipher_class.decrypt(
-            kdf_class(kdf_options),
-            passphrase,
-            cipher_bytes
-        )
-
-        decipher_byte_stream = PascalStyleByteStream(decipher_bytes)
+            if issubclass(cipher_class, ConfidentialityIntegrityCipher):
+                cipher_bytes += byte_stream.read_fixed_bytes(
+                    cipher_class.TAG_LENGTH
+                )
 
-        decipher_bytes_header = \
-            decipher_byte_stream.read_from_format_instructions_dict(
-                cls.DECIPHER_BYTES_HEADER_FORMAT_INSTRUCTIONS_DICT
+            decipher_bytes = cipher_class.decrypt(
+                kdf_class(kdf_options),
+                passphrase,
+                cipher_bytes
             )
 
-        if decipher_bytes_header['check_int_1'] \
-                != decipher_bytes_header['check_int_2']:
-            warnings.warn('Cipher header check numbers do not match')
+            decipher_byte_stream = PascalStyleByteStream(decipher_bytes)
 
-        initlist = []
-        for i in range(num_keys):
-            initlist.append(
-                PublicPrivateKeyPair(
-                    public_key_list[i],
-                    PrivateKey.from_byte_stream(decipher_byte_stream)
-                )
-            )
-            if initlist[i].public.header['key_type'] \
-                    != initlist[i].private.header['key_type']:
-                warnings.warn(
-                    f'Inconsistency between private and public '
-                    f'key types for key {i}'
+            decipher_bytes_header = \
+                decipher_byte_stream.read_from_format_instructions_dict(
+                    cls.DECIPHER_BYTES_HEADER_FORMAT_INSTRUCTIONS_DICT
                 )
-            if not all(
-                (
-                    initlist[i].public.params[k] ==
-                    initlist[i].private.params[k]
-                ) for k in (
-                    initlist[i].public.params.keys() &
-                    initlist[i].private.params.keys()
+
+            if decipher_bytes_header['check_int_1'] \
+                    != decipher_bytes_header['check_int_2']:
+                warnings.warn('Cipher header check numbers do not match')
+
+            initlist = []
+            for i in range(num_keys):
+                initlist.append(
+                    PublicPrivateKeyPair(
+                        public_key_list[i],
+                        PrivateKey.from_byte_stream(decipher_byte_stream)
+                    )
                 )
+                if initlist[i].public.header['key_type'] \
+                        != initlist[i].private.header['key_type']:
+                    warnings.warn(
+                        f'Inconsistency between private and public '
+                        f'key types for key {i}'
+                    )
+                if not all(
+                    (
+                        initlist[i].public.params[k] ==
+                        initlist[i].private.params[k]
+                    ) for k in (
+                        initlist[i].public.params.keys() &
+                        initlist[i].private.params.keys()
+                    )
+                ):
+                    warnings.warn(
+                        f'Inconsistency between private and public '
+                        f'values for key {i}'
+                    )
+
+            decipher_padding = decipher_byte_stream.read()
+
+            if (
+                len(decipher_byte_stream.getvalue()) %
+                    cipher_class.BLOCK_SIZE != 0
+            ) or not (
+                bytes(
+                    range(1, 1 + cipher_class.BLOCK_SIZE)
+                ).startswith(decipher_padding)
             ):
-                warnings.warn(
-                    f'Inconsistency between private and public '
-                    f'values for key {i}'
-                )
-
-        decipher_padding = decipher_byte_stream.read()
-
-        if (
-            len(decipher_byte_stream.getvalue()) %
-                cipher_class.BLOCK_SIZE != 0
-        ) or not (
-            bytes(
-                range(1, 1 + cipher_class.BLOCK_SIZE)
-            ).startswith(decipher_padding)
-        ):
-            warnings.warn('Incorrect padding at end of ciphertext')
+                warnings.warn('Incorrect padding at end of ciphertext')
+        except ValueError as e:
+            raise e
+        except EOFError as e:
+            raise ValueError('Premature EOF detected while parsing key.')
+        except e:
+            raise ValueError('Unexpected error condition reached.')
 
         return cls(
             initlist,

programming_language: Python
repo_url: https://github.com/scottcwang/openssh_key_parser
commit_details: repo_url: https://github.com/scottcwang/openssh_key_parser
author: Michael Doyle
author_date: 2022-06-22 10:49:37-04:00
msg: Changed an exception message to prevent possible disclosures of keying material.
repository: repo_url: https://github.com/scottcwang/openssh_key_parser
repo_name: scottcwang/openssh_key_parser
repo_language: Python
owner: scottcwang
file_changes: filename: pascal_style_byte_stream.py
change_type: ModificationType.MODIFY
diff: @@ -236,7 +236,8 @@ class PascalStyleByteStream(io.BytesIO):
         """
         read_bytes = self.read(num_bytes)
         if len(read_bytes) < num_bytes:
-            raise EOFError(read_bytes)
+            raise EOFError("Fewer than 'num_bytes' bytes remaining in the "
+                    "underlying bytestream")
         return read_bytes
 
     def read_pascal_bytes(self, string_length_size: int) -> bytes:

programming_language: Python
repo_url: https://github.com/scottcwang/openssh_key_parser
commit_details: repo_url: https://github.com/scottcwang/openssh_key_parser
author: scottcwang
author_date: 2022-06-24 22:49:01-05:00
msg: Merge pull request #5 from mike-arnica/security-review

Fix insecure/undeclared raised errors; initial commit of scripts for atheris fuzzer
repository: repo_url: https://github.com/scottcwang/openssh_key_parser
repo_name: scottcwang/openssh_key_parser
repo_language: Python
owner: scottcwang