CVE-2023-1786 (Python, published 2023-04-26T23:15Z)
Description: value: Sensitive data could be exposed in logs of cloud-init before version 23.1.2. An attacker could use this information to find hashed passwords and possibly escalate their privilege.
Severity: nan (CVSS v3 base score: 5.5)
Weakness: CWE-532 \u2014 Insertion of Sensitive Information into Log File
Information written to log files can be of a sensitive nature and give valuable guidance to an attacker or expose sensitive user information.
Repository: https://github.com/canonical/cloud-init
Files changed:
cloudinit/sources/DataSourceLXD.py
cloudinit/sources/DataSourceVultr.py
cloudinit/sources/__init__.py
cloudinit/stages.py
tests/unittests/sources/test_init.py
tests/unittests/test_stages.py
Commit message: Make user/vendor data sensitive and remove log permissions (#2144)

Because user data and vendor data may contain sensitive information,
this commit ensures that any user data or vendor data written to
instance-data.json gets redacted and is only available to root user.

Also, modify the permissions of cloud-init.log to be 640, so that
sensitive data leaked to the log isn't world readable.
Additionally, remove the logging of user data and vendor data to
cloud-init.log from the Vultr datasource.

LP: #2013967
CVE: CVE-2023-1786

Vulnerable code:
from typing import Any, Dict, List, Optional, Union, cast
    sensitive_metadata_keys = (
        "merged_cfg",
        LOG.debug("Vultr Vendor Config:")
        LOG.debug(util.json_dumps(self.metadata["vendor-data"]))
        if self.userdata_raw is not None:
            LOG.debug("User-Data:")
            LOG.debug(self.userdata_raw)

    print(util.json_dumps(sysinfo))
    print(util.json_dumps(config))
        if key in sensitive_keys or sub_key_path in sensitive_keys:
            if isinstance(obj[path], dict) and path != path_parts[-1]:
        obj[path] = redact_value
            util.ensure_file(log_file, mode=0o640, preserve_mode=True)
        self.assertCountEqual(expected, redacted)
    def test_existing_file_permissions_are_not_modified(self, init, tmpdir):
        """If the log file already exists, we should not modify its permissions
        # Use a mode that will never be made the default so this test will
        # always be valid
        mode = 0o606
        log_file.chmod(mode)
        assert mode == stat.S_IMODE(log_file.stat().mode)

Fixed code:
from typing import Any, Dict, List, Optional, Tuple, Union, cast
    sensitive_metadata_keys: Tuple[
        str, ...
    ] = sources.DataSource.sensitive_metadata_keys + (
        "cloud-init.user-data",
        "cloud-init.vendor-data",
from typing import Tuple

    sensitive_metadata_keys: Tuple[
        str, ...
    ] = sources.DataSource.sensitive_metadata_keys + ("startup-script",)

        if (
            key.lower() in sensitive_keys
            or sub_key_path.lower() in sensitive_keys
        ):
    # While 'sensitive_keys' should already sanitized to only include what
    # is in metadata, it is possible keys will overlap. For example, if
    # "merged_cfg" and "merged_cfg/ds/userdata" both match, it's possible that
    # "merged_cfg" will get replaced first, meaning "merged_cfg/ds/userdata"
    # no longer represents a valid key.
    # Thus, we still need to do membership checks in this function.
            if (
                path in obj
                and isinstance(obj[path], dict)
                and path != path_parts[-1]
            ):
        if path in obj:
            obj[path] = redact_value
        "userdata",
        "user-data",
        "user_data",
        "vendordata",
        "vendor-data",
        # Provide ds/vendor_data to avoid redacting top-level
        #  "vendor_data": {enabled: True}
        "ds/vendor_data",
            # At this point the log file should have already been created
            # in the setupLogging function of log.py
            util.ensure_file(log_file, mode=0o640, preserve_mode=False)
                "someother": {
                    "nested": {
                        "userData": "HIDE ME",
                    }
                },
                "VENDOR-DAta": "HIDE ME TOO",
                "userdata",
                "user-data",
                "user_data",
                "vendordata",
                "vendor-data",
                "ds/vendor_data",
                "ds/meta_data/VENDOR-DAta",
                "ds/meta_data/someother/nested/userData",
                "cloud_id": "subclasscloudname",
                    "VENDOR-DAta": REDACT_SENSITIVE_VALUE,
                    "someother": {
                        "nested": {"userData": REDACT_SENSITIVE_VALUE}
                    },
        self.assertEqual(expected, redacted)
                "userdata",
                "user-data",
                "user_data",
                "vendordata",
                "vendor-data",
                "ds/vendor_data",
    def test_existing_file_permissions(self, init, tmpdir):
        """Test file permissions are set as expected.

        CIS Hardening requires 640 permissions. These permissions are
        currently hardcoded on every boot, but if there's ever a reason
        to change this, we need to then ensure that they
        are *not* set every boot.
        # Use a mode that will never be made the default so this test will
        # always be valid
        log_file.chmod(0o606)
        assert 0o640 == stat.S_IMODE(log_file.stat().mode)