Repository: nautobot/nautobot

Issue:
GraphiQL JS XSS Vulnerability
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Python version: 3.6
* Nautobot version: 1.1.4

### Summary
We have a client that their Webinspect scanner has reported the XSS flaw within GraphiQL JS package that we're using.

https://github.com/graphql/graphiql/blob/main/docs/security/2021-introspection-schema-xss.md

Discussed internally and don't think we're actually affected by it, but it's showing up in scans due to advisories.

To remedy, we should upgrade GraphiQL to at least 1.4.7, but the latest is 1.5.1.


Discussion:
Thanks for the report!

Resolving patch:
diff --git a/nautobot/extras/models/change_logging.py b/nautobot/extras/models/change_logging.py
--- a/nautobot/extras/models/change_logging.py
+++ b/nautobot/extras/models/change_logging.py
@@ -8,6 +8,7 @@
 from nautobot.utilities.utils import serialize_object
 from nautobot.core.models import BaseModel
 from nautobot.extras.choices import ObjectChangeActionChoices
+from nautobot.extras.utils import extras_features
 
 
 #
@@ -40,6 +41,7 @@ def to_objectchange(self, action):
         )
 
 
+@extras_features("graphql")
 class ObjectChange(BaseModel):
     """
     Record a change to an object and the user account associated with that change. A change record may optionally
diff --git a/nautobot/extras/tables.py b/nautobot/extras/tables.py
--- a/nautobot/extras/tables.py
+++ b/nautobot/extras/tables.py
@@ -480,9 +480,9 @@ class Meta(BaseTable.Meta):
             "completed",
             "user",
             "status",
-            "logs",
+            "summary",
         )
-        default_columns = ("pk", "created", "related_object", "user", "status", "logs")
+        default_columns = ("pk", "created", "related_object", "user", "status", "summary")
 
 
 #


Test patch:
diff --git a/nautobot/extras/tests/test_changelog.py b/nautobot/extras/tests/test_changelog.py
--- a/nautobot/extras/tests/test_changelog.py
+++ b/nautobot/extras/tests/test_changelog.py
@@ -1,7 +1,9 @@
 from django.contrib.contenttypes.models import ContentType
 from django.urls import reverse
+from django.test import override_settings
 from rest_framework import status
 
+from nautobot.core.graphql import execute_query
 from nautobot.dcim.models import Site
 from nautobot.extras.choices import CustomFieldTypeChoices, ObjectChangeActionChoices
 from nautobot.extras.models import CustomField, CustomFieldChoice, ObjectChange, Status, Tag
@@ -292,3 +294,22 @@ def test_delete_object(self):
         self.assertEqual(oc.object_data["custom_fields"]["my_field"], "ABC")
         self.assertEqual(oc.object_data["custom_fields"]["my_field_select"], "Bar")
         self.assertEqual(oc.object_data["tags"], ["Tag 1", "Tag 2"])
+
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
+    def test_get_graphql_object(self):
+        """Test GET with changelogs via GraphQL."""
+        site_payload = {
+            "name": "Test Site 1",
+            "slug": "test-site-1",
+            "status": "active",
+        }
+        self.add_permissions("dcim.add_site")
+
+        sites_url = reverse("dcim-api:site-list")
+        new_site_response = self.client.post(sites_url, site_payload, format="json", **self.header)
+        self.assertHttpStatus(new_site_response, status.HTTP_201_CREATED)
+
+        gql_payload = '{query: object_changes(q: "") { object_repr } }'
+        resp = execute_query(gql_payload, user=self.user).to_dict()
+        self.assertFalse(resp["data"].get("error"))
+        self.assertEqual(first=site_payload["name"], second=resp["data"]["query"][0].get("object_repr", ""))