CVE-2022-4595 (published 2022-12-18T11:15Z)

A vulnerability classified as problematic has been found in django-openipam. This affects an unknown part of the file openipam/report/templates/report/exposed_hosts.html. The manipulation of the argument description leads to cross site scripting. It is possible to initiate the attack remotely. The name of the patch is a6223a1150d60cd036106ba6a8e676c1bfc3cc85. It is recommended to apply a patch to fix this issue. The identifier VDB-216189 was assigned to this vulnerability.

CVSS v3 base score: 6.1, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-707
cwe_name: Improper Neutralization
description: The product does not ensure or incorrectly ensures that structured messages or data are well-formed and that certain security properties are met before being read from an upstream component or sent to a downstream component.
extended_description: 
            If a message is malformed, it may cause the message to be incorrectly interpreted.
            Neutralization is an abstract term for any technique that ensures that input (and output) conforms with expectations and is "safe."  This can be done by:
	    
	      checking that the input/output is already "safe" (e.g. validation)
	      transformation of the input/output to be "safe" using techniques such as filtering, encoding/decoding, escaping/unescaping, quoting/unquoting, or canonicalization
	      preventing the input/output from being directly provided by an attacker (e.g. "indirect selection" that maps externally-provided values to internally-controlled values)
	      preventing the input/output from being processed at all
	    
            This weakness typically applies in cases where the product prepares a control message that another process must act on, such as a command or query, and malicious input that was intended as data, can enter the control plane instead. However, this weakness also applies to more general cases where there are not always control implications.
         
         

Fix information:
repo_url: https://github.com/openipam/django-openipam
commit_details: repo_url: https://github.com/openipam/django-openipam
author: Logan Hunt
author_date: 2022-11-11 10:55:54-07:00
msg: Exposed Hosts Improvements (#205)

* Fixed XSS issue by rendering description as text. Also, drastically improved performance of exposed host report via a ridiculous sql query.

* Run black & remove unused imports

* Remove trailing whitespace

* There was still a trailing whitespace trailing behind
repository: repo_url: https://github.com/openipam/django-openipam
repo_name: openipam/django-openipam
description: Django refactor of openipam foo
repo_language: Python
owner: openipam
file_changes: filename: report.py
change_type: ModificationType.MODIFY
diff: @@ -11,26 +11,25 @@ from rest_framework.decorators import api_view, renderer_classes, permission_cla
 
 from rest_framework_csv.renderers import CSVRenderer
 
+from django.db import connection
 from django.db.models.aggregates import Count
 from django.http import HttpResponse
-from django.contrib.contenttypes.models import ContentType
 from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Permission
 from django.apps import apps
-from django.db.models import Q, F
+from django.db.models import Q
 from django.utils import timezone
 
-from openipam.hosts.models import Host
+from openipam.hosts.models import Host, Attribute
 from openipam.report.models import Ports
 from openipam.report.models import database_connect, database_close
 from openipam.network.models import Network, Lease, Address
 from openipam.dns.models import DnsRecord
-from openipam.conf.ipam_settings import CONFIG, CONFIG_DEFAULTS
+from openipam.conf.ipam_settings import CONFIG
 from openipam.conf.settings import get_buildingmap_data
 
 from functools import reduce
 
-from guardian.models import UserObjectPermission, GroupObjectPermission
-
 import copy
 
 import qsstats
@@ -424,9 +423,10 @@ class ServerHostCSVRenderer(CSVRenderer):
         "hostname",
         "mac",
         "description",
-        "master_ip_address",
+        "addresses",
         "user_owners",
         "group_owners",
+        "nac_profiles",
     ]
 
 
@@ -435,56 +435,43 @@ class ServerHostView(APIView):
     renderer_classes = (BrowsableAPIRenderer, JSONRenderer, ServerHostCSVRenderer)
 
     def get(self, request, format=None, **kwargs):
-        hosts = (
-            Host.objects.prefetch_related("addresses")
-            .filter(
-                structured_attributes__structured_attribute_value__attribute__name="nac-profile",
-                structured_attributes__structured_attribute_value__value__startswith=CONFIG_DEFAULTS[
-                    "NAC_PROFILE_IS_SERVER_PREFIX"
-                ],
-            )
-            .annotate(
-                nac_profile=F(
-                    "structured_attributes__structured_attribute_value__value"
-                ),
-            )
-        )
-
-        user_perms_prefetch = UserObjectPermission.objects.select_related(
-            "permission", "user"
-        ).filter(
-            content_type=ContentType.objects.get_for_model(Host),
-            object_pk__in=[str(host.mac) for host in hosts],
-            permission__codename="is_owner_host",
-        )
-        group_perms_prefetch = GroupObjectPermission.objects.select_related(
-            "permission", "group"
-        ).filter(
-            content_type=ContentType.objects.get_for_model(Host),
-            object_pk__in=[str(host.mac) for host in hosts],
-            permission__codename="is_owner_host",
+        nac_profile_attribute = Attribute.objects.get(name="nac-profile")
+        host_owner_permission = Permission.objects.get(codename="is_owner_host")
+
+        cursor = connection.cursor()
+        cursor.execute(
+            """
+            SELECT hosts.hostname AS hostname,
+                   hosts.mac AS mac,
+                   hosts.description AS description,
+                   STRING_AGG(DISTINCT((SELECT CAST(addresses.address AS VARCHAR))), ', ') AS addresses,
+                   STRING_AGG(DISTINCT(users.username), ', ') AS users,
+                   STRING_AGG(DISTINCT(groups.name), ', ') AS groups,
+                   STRING_AGG(DISTINCT(host_attr_vals.value), ', ') AS nac_profiles
+            FROM hosts
+                JOIN structured_attributes_to_hosts AS host_attrs ON hosts.mac=host_attrs.mac
+                JOIN structured_attribute_values AS host_attr_vals ON host_attrs.avid=host_attr_vals.id
+                LEFT JOIN guardian_userobjectpermission AS uop ON uop.object_pk=(SELECT CAST(hosts.mac AS VARCHAR)) AND uop.permission_id = %s
+                LEFT JOIN guardian_groupobjectpermission AS gop ON gop.object_pk=(SELECT CAST(hosts.mac AS VARCHAR)) AND gop.permission_id=%s
+                LEFT JOIN addresses ON hosts.mac=addresses.mac
+                LEFT JOIN users ON uop.user_id=users.id
+                LEFT JOIN groups ON gop.group_id=groups.id
+            WHERE host_attr_vals.aid = %s
+            AND STARTS_WITH(host_attr_vals.value, %s)
+            GROUP BY hosts.mac, hosts.hostname, hosts.description
+            """,
+            [
+                host_owner_permission.id,
+                host_owner_permission.id,
+                nac_profile_attribute.id,
+                CONFIG["NAC_PROFILE_IS_SERVER_PREFIX"],
+            ],
         )
 
-        data = []
-        for host in hosts:
-            owners = host.get_owners(
-                name_only=True,
-                user_perms_prefetch=user_perms_prefetch,
-                group_perms_prefetch=group_perms_prefetch,
-            )
-            data.append(
-                {
-                    "hostname": host.hostname,
-                    "mac": str(host.mac),
-                    "description": host.description,
-                    "master_ip_address": host.ip_addresses[0]
-                    if host.ip_addresses
-                    else None,
-                    "user_owners": ", ".join(owners[0]),
-                    "group_owners": ", ".join(owners[1]),
-                    "nac_profile": host.nac_profile,
-                }
-            )
+        data = [
+            dict(zip([col[0] for col in cursor.description], row))
+            for row in cursor.fetchall()
+        ]
 
         if request.accepted_renderer.format == "json":
             return Response({"data": data}, status=status.HTTP_200_OK)

programming_language: Python
filename: exposed_hosts.html
change_type: ModificationType.MODIFY
diff: @@ -19,6 +19,8 @@
 <script type="text/javascript" src="{% static 'core/js/dataTables.bootstrap.js' %}"></script>
 
 <script type="text/javascript">
+  const escapeHtml = (t) => $('<div />').text(t).html();  
+
   $(function () {
     $("#server_link").addClass("active");
     $("#reports_dropdown").addClass("active");
@@ -26,29 +28,34 @@
     $("#result_list").DataTable({
       ajax: "/api/reports/serverhosts?format=json",
       columns: [{
-          data: "hostname"
+          data: "hostname",
+          orderable: true,
         },
         {
-          data: "mac"
+          data: "mac",
+          orderable: true,
+          render: (t) => `<a href="/hosts/${escapeHtml(t).replace(/\:/g, "")}/detail">${escapeHtml(t)}</a>`,
         },
         {
           data: "description",
-          orderable: false
+          orderable: false,
+          render: escapeHtml,
         },
         {
-          data: "master_ip_address"
+          data: "addresses",
+          orderable: true,
         },
         {
-          data: "user_owners",
-          orderable: false
+          data: "users",
+          orderable: false,
         },
         {
-          data: "group_owners",
-          orderable: false
+          data: "groups",
+          orderable: false,
         },
         {
-          data: "nac_profile",
-          orderable: false
+          data: "nac_profiles",
+          orderable: false,
         },
       ],
       paging: false,
@@ -72,10 +79,10 @@
           <th scope="col">Host</th>
           <th scope="col">Mac Address</th>
           <th scope="col">Description</th>
-          <th scope="col">IP Address</th>
-          <th scope="col">User Owners</th>
-          <th scope="col">Group Owners</th>
-          <th scope="col">NAC Profile</th>
+          <th scope="col">IP Address(es)</th>
+          <th scope="col">User Owner(s)</th>
+          <th scope="col">Group Owner(s)</th>
+          <th scope="col">NAC Profile(s)</th>
         </thead>
       </tr>
       <tbody></tbody>

programming_language: HTML