CVE-2023-46128 (published 2023-10-25T18:17Z)

Nautobot is a Network Automation Platform built as a web application atop the Django Python framework with a PostgreSQL or MySQL database. In Nautobot 2.0.x, certain REST API endpoints, in combination with the `?depth=<N>` query parameter, can expose hashed user passwords as stored in the database to any authenticated user with access to these endpoints. The passwords are not exposed in plaintext. This vulnerability has been patched in version 2.0.3.\n\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-312
cwe_name: Cleartext Storage of Sensitive Information
description: The product stores sensitive information in cleartext within a resource that might be accessible to another control sphere.
extended_description: 
	 Because the information is stored in cleartext (i.e., unencrypted), attackers could potentially read it. Even if the information is encoded in a way that is not human-readable, certain techniques could determine which encoding is being used, then decode the information.
	 When organizations adopt cloud services, it can be easier for attackers to access the data from anywhere on the Internet.
	 In some systems/environments such as cloud, the use of "double encryption" (at both the software and hardware layer) might be required, and the developer might be solely responsible for both layers, instead of shared responsibility with the administrator of the broader system/environment.
       
         

Fix information:
repo_url: https://github.com/nautobot/nautobot
commit_details: repo_url: https://github.com/nautobot/nautobot
author: Glenn Matthews
author_date: 2023-10-23 16:56:09-04:00
msg: Fix `Meta` inheritance in nested serializer classes (#4692)

* Add test coverage

* Fix issue

* Change fragment

* Add more change fragments

* Refine test implementation

* Add reference to GHSA

* Add admonition about use of fields = __all__ on serializers

* Add PR number
repository: repo_url: https://github.com/nautobot/nautobot
repo_name: nautobot/nautobot
description: Network Source of Truth & Network Automation Platform
repo_language: Python
owner: nautobot
file_changes: filename: 4692.fixed
change_type: ModificationType.ADD
diff: @@ -0,0 +1 @@
+Fixed incorrect inheritance of `Meta` attributes into nested serializers (`depth >= 1`).

programming_language: Batchfile
filename: 4692.housekeeping
change_type: ModificationType.ADD
diff: @@ -0,0 +1 @@
+Added check in REST API generic test cases to detect strings like `password` and `sha256` that shouldn't generally appear in REST API responses.

programming_language: SQL
filename: 4692.security
change_type: ModificationType.ADD
diff: @@ -0,0 +1 @@
+Fixed potential exposure of hashed user password data on certain REST API endpoints when using the `?depth=1` query parameter. For more details, please refer to [GHSA-r2hw-74xv-4gqp](https://github.com/nautobot/nautobot/security/advisories/GHSA-r2hw-74xv-4gqp).

programming_language: Markdown
filename: utils.py
change_type: ModificationType.MODIFY
diff: @@ -314,14 +314,9 @@ def nested_serializer_factory(relation_info, nested_depth):
         base_serializer_class = get_serializer_for_model(relation_info.related_model)
 
         class NautobotNestedSerializer(base_serializer_class):
-            class Meta:
-                model = relation_info.related_model
+            class Meta(base_serializer_class.Meta):
                 is_nested = True
                 depth = nested_depth - 1
-                if hasattr(base_serializer_class.Meta, "fields"):
-                    fields = base_serializer_class.Meta.fields
-                if hasattr(base_serializer_class.Meta, "exclude"):
-                    exclude = base_serializer_class.Meta.exclude
 
         NautobotNestedSerializer.__name__ = nested_serializer_name
         NESTED_SERIALIZER_CACHE[nested_serializer_name] = NautobotNestedSerializer

programming_language: Python
filename: api.py
change_type: ModificationType.MODIFY
diff: @@ -73,6 +73,31 @@ class APITestCase(views.ModelTestCase):
         viewname = lookup.get_route_for_model(self.model, "list", api=True)
         return reverse(viewname)
 
+    VERBOTEN_STRINGS = (
+        "password",
+        # https://docs.djangoproject.com/en/3.2/topics/auth/passwords/#included-hashers
+        "argon2",
+        "bcrypt",
+        "crypt",
+        "md5",
+        "pbkdf2",
+        "scrypt",
+        "sha1",
+        "sha256",
+        "sha512",
+    )
+
+    def assert_no_verboten_content(self, response):
+        """
+        Check an API response for content that should not be exposed in the API.
+
+        If a specific API has a false failure here (maybe it has security-related strings as model flags or something?),
+        its test case should overload self.VERBOTEN_STRINGS appropriately.
+        """
+        response_raw_content = response.content.decode(response.charset)
+        for verboten in self.VERBOTEN_STRINGS:
+            self.assertNotIn(verboten, response_raw_content)
+
 
 @tag("unit")
 class APIViewTestCases:
@@ -150,6 +175,8 @@ class APIViewTestCases:
             # Fields that should be absent by default (opt-in fields):
             self.assertNotIn("computed_fields", response.data)
             self.assertNotIn("relationships", response.data)
+            # Content that should never be present:
+            self.assert_no_verboten_content(response)
 
             # If opt-in fields are supported on this model, make sure they can be opted into
 
@@ -302,6 +329,7 @@ class APIViewTestCases:
             self.assertIsInstance(response.data, dict)
             self.assertIn("results", response.data)
             self.assertEqual(len(response.data["results"]), self._get_queryset().count())
+            self.assert_no_verboten_content(response)
 
             for response_data in response.data["results"]:
                 for field in depth_fields:
@@ -316,7 +344,8 @@ class APIViewTestCases:
                             url = response_data[field]["url"]
                             pk = response_data[field]["id"]
                             object_type = response_data[field]["object_type"]
-                            # The response should be a brief API object, containing an ID, object_type, and URL ending in the UUID of the relevant object
+                            # The response should be a brief API object, containing an ID, object_type, and a
+                            # URL ending in the UUID of the relevant object:
                             # http://nautobot.example.com/api/circuits/providers/<uuid>/
                             #                                                    ^^^^^^
                             self.assertTrue(is_uuid(url.split("/")[-2]))
@@ -340,6 +369,7 @@ class APIViewTestCases:
             self.assertIsInstance(response.data, dict)
             self.assertIn("results", response.data)
             self.assertEqual(len(response.data["results"]), self._get_queryset().count())
+            self.assert_no_verboten_content(response)
 
             for response_data in response.data["results"]:
                 for field in depth_fields:
@@ -392,6 +422,7 @@ class APIViewTestCases:
             self.assertIsInstance(response.data, dict)
             self.assertIn("results", response.data)
             self.assertEqual(len(response.data["results"]), 2)
+            self.assert_no_verboten_content(response)
 
         @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
         def test_list_objects_filtered(self):

programming_language: Python
filename: code-updates.md
change_type: ModificationType.MODIFY
diff: @@ -123,6 +123,9 @@ App Model Serializers for any models that could have a Generic Foreign Key or a
 
 After removing existing `NestedSerializers`, you can change the `fields` attribute in your serializers' `class Meta` to `__all__` and that will automatically include all the model's fields in the serializer, including related-model fields that would previously have required a reference to a `NestedSerializer`. If you want to exclude certain fields of the model, you can specify a list of fields you want to display in the `fields` attribute instead.
 
+!!! warning
+    Use caution around `fields = "__all__"` -- if your model has any fields that should _not_ be exposed in the REST API, you should avoid using `"__all__"` and instead use an explicit `fields` list to ensure that such fields are not exposed. In some cases, it may be appropriate to use `"__all__"` in combination with flags such as `write_only=True` on specific fields, but proceed with caution and examine the REST API data carefully to ensure that its contents are as expected.
+
 Include all model attributes:
 
 ```python

programming_language: Markdown