CVE-2022-4595 (Python, published 2022-12-18T11:15Z)
Description: value: 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.
Severity: nan (CVSS v3 base score: 6.1)
Weakness: CWE-707 \u2014 Improper Neutralization
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.
Repository: https://github.com/openipam/django-openipam
Files changed:
openipam/api/views/report.py
openipam/report/templates/report/exposed_hosts.html
Commit message: 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

Vulnerable code:
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q, F
from openipam.hosts.models import Host
from openipam.conf.ipam_settings import CONFIG, CONFIG_DEFAULTS
from guardian.models import UserObjectPermission, GroupObjectPermission

        "master_ip_address",
        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",
        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: "hostname"
          data: "mac"
          orderable: false
          data: "master_ip_address"
          data: "user_owners",
          orderable: false
          data: "group_owners",
          orderable: false
          data: "nac_profile",
          orderable: false
          <th scope="col">IP Address</th>
          <th scope="col">User Owners</th>
          <th scope="col">Group Owners</th>
          <th scope="col">NAC Profile</th>

Fixed code:
from django.db import connection
from django.contrib.auth.models import Permission
from django.db.models import Q
from openipam.hosts.models import Host, Attribute
from openipam.conf.ipam_settings import CONFIG
        "addresses",
        "nac_profiles",
        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 = [
            dict(zip([col[0] for col in cursor.description], row))
            for row in cursor.fetchall()
        ]
  const escapeHtml = (t) => $('<div />').text(t).html();  

          data: "hostname",
          orderable: true,
          data: "mac",
          orderable: true,
          render: (t) => `<a href="/hosts/${escapeHtml(t).replace(/\:/g, "")}/detail">${escapeHtml(t)}</a>`,
          orderable: false,
          render: escapeHtml,
          data: "addresses",
          orderable: true,
          data: "users",
          orderable: false,
          data: "groups",
          orderable: false,
          data: "nac_profiles",
          orderable: false,
          <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>