CVE-2022-2084: Insertion of Sensitive Information into Log File (CWE-532)
value: Sensitive data could be exposed in world readable logs of cloud-init before version 22.3 when schema failures are reported. This leak could include hashed passwords.
CWE description: Information written to log files can be of a sensitive nature and give valuable guidance to an attacker or expose sensitive user information.
CVSS v3 base score: 5.5
Published: 2023-04-19T22:15Z
Repository: https://github.com/canonical/cloud-init
Language: Python
Files changed: cloudinit/cmd/main.py, cloudinit/config/schema.py, tests/integration_tests/modules/test_cli.py, tests/unittests/config/test_schema.py
Commit date: 2022-06-29 17:27:44 -0500
Commit message: Remove schema errors from log (#1551)

When schema errors are encountered, the section of userdata in question
gets printed to the cloud-init log. As this could contain sensitive
data, so log a generic warning instead and redirect user to run
cloud-init schema --system as root.

LP: #1978422
CVE: 2022-2084
Diff: diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py
index c63034786..4f157870b 100755
--- a/cloudinit/cmd/main.py
+++ b/cloudinit/cmd/main.py
@@ -453,11 +453,13 @@ def main_init(name, args):
         util.logexc(LOG, "Consuming user data failed!")
         return (init.datasource, ["Consuming user data failed!"])
 
     # Validate user-data adheres to schema definition
     if os.path.exists(init.paths.get_ipath_cur("userdata_raw")):
-        validate_cloudconfig_schema(config=init.cfg, strict=False)
+        validate_cloudconfig_schema(
+            config=init.cfg, strict=False, log_details=False
+        )
     else:
         LOG.debug("Skipping user-data validation. No user-data found.")
 
     apply_reporting_cfg(init.cfg)
 
diff --git a/cloudinit/config/schema.py b/cloudinit/config/schema.py
index a60ceb112..1e29ae5aa 100644
--- a/cloudinit/config/schema.py
+++ b/cloudinit/config/schema.py
@@ -196,10 +196,11 @@ def validate_cloudconfig_metaschema(validator, schema: dict, throw=True):
 def validate_cloudconfig_schema(
     config: dict,
     schema: dict = None,
     strict: bool = False,
     strict_metaschema: bool = False,
+    log_details: bool = True,
 ):
     """Validate provided config meets the schema definition.
 
     @param config: Dict of cloud configuration settings validated against
         schema. Ignored if strict_metaschema=True
@@ -208,10 +209,13 @@ def validate_cloudconfig_schema(
        global schema.
     @param strict: Boolean, when True raise SchemaValidationErrors instead of
        logging warnings.
     @param strict_metaschema: Boolean, when True validates schema using strict
        metaschema definition at runtime (currently unused)
+    @param log_details: Boolean, when True logs details of validation errors.
+       If there are concerns about logging sensitive userdata, this should
+       be set to False.
 
     @raises: SchemaValidationError when provided config does not validate
         against the provided schema.
     @raises: RuntimeError when provided config sourced from YAML is not a dict.
     """
@@ -232,16 +236,21 @@ def validate_cloudconfig_schema(
     for error in sorted(validator.iter_errors(config), key=lambda e: e.path):
         path = ".".join([str(p) for p in error.path])
         errors += ((path, error.message),)
     if errors:
         if strict:
+            # This could output/log sensitive data
             raise SchemaValidationError(errors)
-        else:
+        if log_details:
             messages = ["{0}: {1}".format(k, msg) for k, msg in errors]
-            LOG.warning(
-                "Invalid cloud-config provided:\n%s", "\n".join(messages)
+            details = "\n" + "\n".join(messages)
+        else:
+            details = (
+                "Please run 'sudo cloud-init schema --system' to "
+                "see the schema errors."
             )
+        LOG.warning("Invalid cloud-config provided: %s", details)
 
 
 def annotated_cloudconfig_file(
     cloudconfig, original_content, schema_errors, schemamarks
 ):
diff --git a/tests/integration_tests/modules/test_cli.py b/tests/integration_tests/modules/test_cli.py
index e878176f4..4b8f53a8d 100644
--- a/tests/integration_tests/modules/test_cli.py
+++ b/tests/integration_tests/modules/test_cli.py
@@ -16,15 +16,22 @@ runcmd:
 INVALID_USER_DATA_HEADER = """\
 runcmd:
   - echo 'hi' > /var/tmp/test
 """
 
+# The '-' in 'hashed-password' fails schema validation
 INVALID_USER_DATA_SCHEMA = """\
 #cloud-config
-updates:
- notnetwork: -1
-apt_pipelining: bogus
+users:
+  - default
+  - name: newsuper
+    gecos: Big Stuff
+    groups: users, admin
+    sudo: ALL=(ALL) NOPASSWD:ALL
+    hashed-password: asdfasdf
+    shell: /bin/bash
+    lock_passwd: true
 """
 
 
 @pytest.mark.user_data(VALID_USER_DATA)
 def test_valid_userdata(client: IntegrationInstance):
@@ -67,15 +74,16 @@ def test_invalid_userdata_schema(client: IntegrationInstance):
     """
     result = client.execute("cloud-init status --long")
     assert result.ok
     log = client.read_from_file("/var/log/cloud-init.log")
     warning = (
-        "[WARNING]: Invalid cloud-config provided:\napt_pipelining: 'bogus'"
-        " is not valid under any of the given schemas\nupdates: Additional"
-        " properties are not allowed ('notnetwork' was unexpected)"
+        "[WARNING]: Invalid cloud-config provided: Please run "
+        "'sudo cloud-init schema --system' to see the schema errors."
     )
     assert warning in log
+    assert "asdfasdf" not in log
+
     result = client.execute("cloud-init status --long")
     if not result.ok:
         raise AssertionError(
             f"Unexpected error from cloud-init status: {result}"
         )
diff --git a/tests/unittests/config/test_schema.py b/tests/unittests/config/test_schema.py
index 1840b70d7..4a41c4c13 100644
--- a/tests/unittests/config/test_schema.py
+++ b/tests/unittests/config/test_schema.py
@@ -284,14 +284,35 @@ class TestValidateCloudConfigSchema:
         validate_cloudconfig_schema({"p1": -1}, schema, strict=False)
         [(module, log_level, log_msg)] = caplog.record_tuples
         assert "cloudinit.config.schema" == module
         assert logging.WARNING == log_level
         assert (
-            "Invalid cloud-config provided:\np1: -1 is not of type 'string'"
+            "Invalid cloud-config provided: \np1: -1 is not of type 'string'"
             == log_msg
         )
 
+    @skipUnlessJsonSchema()
+    def test_validateconfig_schema_sensitive(self, caplog):
+        """When log_details=False, ensure details are omitted"""
+        schema = {
+            "properties": {"hashed_password": {"type": "string"}},
+            "additionalProperties": False,
+        }
+        validate_cloudconfig_schema(
+            {"hashed-password": "secret"},
+            schema,
+            strict=False,
+            log_details=False,
+        )
+        [(module, log_level, log_msg)] = caplog.record_tuples
+        assert "cloudinit.config.schema" == module
+        assert logging.WARNING == log_level
+        assert (
+            "Invalid cloud-config provided: Please run 'sudo cloud-init "
+            "schema --system' to see the schema errors." == log_msg
+        )
+
     @skipUnlessJsonSchema()
     def test_validateconfig_schema_emits_warning_on_missing_jsonschema(
         self, caplog
     ):
         """Warning from validate_cloudconfig_schema when missing jsonschema."""

Vulnerable code:         validate_cloudconfig_schema(config=init.cfg, strict=False)
        else:
            LOG.warning(
                "Invalid cloud-config provided:\n%s", "\n".join(messages)
updates:
 notnetwork: -1
apt_pipelining: bogus
        "[WARNING]: Invalid cloud-config provided:\napt_pipelining: 'bogus'"
        " is not valid under any of the given schemas\nupdates: Additional"
        " properties are not allowed ('notnetwork' was unexpected)"
            "Invalid cloud-config provided:\np1: -1 is not of type 'string'"
Fixed code:         validate_cloudconfig_schema(
            config=init.cfg, strict=False, log_details=False
        )
    log_details: bool = True,
    @param log_details: Boolean, when True logs details of validation errors.
       If there are concerns about logging sensitive userdata, this should
       be set to False.
            # This could output/log sensitive data
        if log_details:
            details = "\n" + "\n".join(messages)
        else:
            details = (
                "Please run 'sudo cloud-init schema --system' to "
                "see the schema errors."
        LOG.warning("Invalid cloud-config provided: %s", details)
# The '-' in 'hashed-password' fails schema validation
users:
  - default
  - name: newsuper
    gecos: Big Stuff
    groups: users, admin
    sudo: ALL=(ALL) NOPASSWD:ALL
    hashed-password: asdfasdf
    shell: /bin/bash
    lock_passwd: true
        "[WARNING]: Invalid cloud-config provided: Please run "
        "'sudo cloud-init schema --system' to see the schema errors."
    assert "asdfasdf" not in log

            "Invalid cloud-config provided: \np1: -1 is not of type 'string'"
    @skipUnlessJsonSchema()
    def test_validateconfig_schema_sensitive(self, caplog):
        """When log_details=False, ensure details are omitted"""
        schema = {
            "properties": {"hashed_password": {"type": "string"}},
            "additionalProperties": False,
        }
        validate_cloudconfig_schema(
            {"hashed-password": "secret"},
            schema,
            strict=False,
            log_details=False,
        )
        [(module, log_level, log_msg)] = caplog.record_tuples
        assert "cloudinit.config.schema" == module
        assert logging.WARNING == log_level
        assert (
            "Invalid cloud-config provided: Please run 'sudo cloud-init "
            "schema --system' to see the schema errors." == log_msg
        )