VYPR
High severityNVD Advisory· Published May 1, 2024· Updated Aug 2, 2024

Reflected Cross-site Scripting potential in all object list views in Nautobot

CVE-2024-32979

Description

Nautobot is a Network Source of Truth and Network Automation Platform built as a web application atop the Django Python framework with a PostgreSQL or MySQL database. It was discovered that due to improper handling and escaping of user-provided query parameters, a maliciously crafted Nautobot URL could potentially be used to execute a Reflected Cross-Site Scripting (Reflected XSS) attack against users. All filterable object-list views in Nautobot are vulnerable. This issue has been fixed in Nautobot versions 1.6.20 and 2.2.3. There are no known workarounds for this vulnerability.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CVE-2024-32979 is a reflected XSS vulnerability in Nautobot's filterable object-list views due to improper escaping of user-supplied query parameters.

Vulnerability

Overview

CVE-2024-32979 describes a reflected cross-site scripting (XSS) vulnerability in Nautobot, a Network Source of Truth and Network Automation Platform built with Django. The flaw originates from improper handling and escaping of user-provided query parameters in filterable object-list views. When a maliciously crafted URL is constructed, the unsanitized parameters are reflected back into the page without proper encoding, allowing an attacker to inject arbitrary JavaScript [1].

Exploitation

Details

Attackers can exploit this vulnerability by convincing a user to click a specially crafted link containing malicious query parameters. No authentication is required to trigger the reflection, as the vulnerable code processes parameters directly from the URL. The fix implemented in the pull request #5647 addresses the issue by adding quotes around data attributes (data-field-value="{{ field.name }}") and by using the URLSearchParams API to safely handle query strings, preventing injection via unescaped values [2][4].

Impact

Successful exploitation results in reflected XSS, enabling the attacker to execute arbitrary JavaScript in the context of the victim's browser session. This can lead to session hijacking, credential theft, or other malicious actions within the Nautobot application. All filterable object-list views are affected, making the attack surface broad [1].

Mitigation

The vulnerability has been patched in Nautobot versions 1.6.20 and 2.2.3. Users are strongly advised to upgrade to these or later versions. No known workarounds exist for this issue, and applying the official patch is the only recommended course of action [1][3].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
nautobotPyPI
>= 1.5.0, < 1.6.201.6.20
nautobotPyPI
>= 2.0.0, < 2.2.32.2.3

Affected products

2
  • ghsa-coords
    Range: >= 1.5.0, < 1.6.20
  • nautobot/nautobotv5
    Range: < 1.6.20

Patches

2
2ea5797ea436

[LTM] Fix quoting of query parameters in list view (#5647)

https://github.com/nautobot/nautobotGlenn MatthewsApr 30, 2024via ghsa
3 files changed · +41 12
  • changes/5647.security+1 0 added
    @@ -0,0 +1 @@
    +Fixed a reflected-XSS vulnerability ([GHSA-jxgr-gcj5-cqqg](https://github.com/nautobot/nautobot/security/advisories/GHSA-jxgr-gcj5-cqqg)) in object-list view rendering of user-provided query parameters.
    
  • nautobot/core/templates/generic/object_list.html+21 12 modified
    @@ -68,7 +68,7 @@ <h1>{% block title %}{{ title }}{% endblock %}</h1>
                         class="remove-filter-param"
                         title="Remove all items"
                         data-field-type="parent"
    -                    data-field-value={{ field.name }}
    +                    data-field-value="{{ field.name }}"
                 >×</span>
                 <ul class="filter-selection-rendered">
                     {% for value in field.values %}
    @@ -79,8 +79,8 @@ <h1>{% block title %}{{ title }}{% endblock %}</h1>
                         <span
                                 class="filter-selection-choice-remove remove-filter-param"
                                 data-field-type="child"
    -                            data-field-parent={{ field.name }}
    -                            data-field-value={{ value.name }}
    +                            data-field-parent="{{ field.name }}"
    +                            data-field-value="{{ value.name }}"
                         >×</span>{{ value.display }}
                     </li>
                     {% endfor %}
    @@ -185,18 +185,19 @@ <h1>{% block title %}{{ title }}{% endblock %}</h1>
     
         // Remove applied filters
         $(".remove-filter-param").on("click", function(){
    -        let query_params = location.search;
    +        let query_params = new URLSearchParams(location.search);
             let type = $(this).attr("data-field-type");
             let field_value = $(this).attr("data-field-value");
    -        let query_string = location.search.substr(1).split("&");
     
             if (type === "parent") {
    -            query_string = query_string.filter(item => item.search(field_value) < 0);
    +            // Remove all instances of this query param
    +            query_params.delete(field_value);
             } else {
    +            // Remove this specific instance of this query param
                 let parent = $(this).attr("data-field-parent");
    -            query_string = query_string.filter(item => item.search(parent + "=" + field_value) < 0)
    +            query_params.delete(parent, field_value);
             }
    -        location.replace("?" + query_string.join("&"))
    +        location.assign("?" + query_params);
         })
     
         // On submit of filter form
    @@ -210,10 +211,18 @@ <h1>{% block title %}{{ title }}{% endblock %}</h1>
             q_field_phantom.val(q_field.val())
             dynamic_form.append(q_field_phantom);
     
    -        let search_query = $("#dynamic-filter-form, #default-filter form").serialize()
    -        // Remove duplicates generated by filter merging on both dynamic and default filter forms.
    -        search_query = "&" + search_query.replace(/([^&]+=[^&]+&)(?=.*\1)/g, "")
    -        location.replace("?" + search_query)
    +        // Get the serialized data from the forms and:
    +        // 1) filter out query_params which values are empty e.g. ?sam=&dan=2 becomes ?dan=2
    +        // 2) combine the two forms into a single set of data without duplicate entries
    +        let search_query = new URLSearchParams()
    +        let dynamic_query = new URLSearchParams(new FormData(document.getElementById("dynamic-filter-form")));
    +        dynamic_query.forEach((value, key) => { if (value != "") { search_query.append(key, value); }});
    +        let default_query = new URLSearchParams(new FormData(document.getElementById("default-filter").firstElementChild));
    +        default_query.forEach((value, key) => {
    +            if (value != "" && !search_query.has(key, value)) { search_query.append(key, value); }
    +        });
    +        $("#FilterForm_modal").modal("hide");
    +        location.assign("?" + search_query);
         })
     
         // On submit of filter search form
    
  • nautobot/core/tests/test_views.py+19 0 modified
    @@ -163,6 +163,25 @@ def test_support_for_both_default_and_dynamic_filter_form_in_ui(self):
                 response.content.decode(response.charset),
             )
     
    +    def test_filtering_crafted_query_params(self):
    +        """Test for reflected-XSS vulnerability GHSA-jxgr-gcj5-cqqg."""
    +        self.add_permissions("dcim.view_location")
    +        query_param = "?location_type=1 onmouseover=alert('hi') foo=bar"
    +        url = reverse("dcim:location_list") + query_param
    +        response = self.client.get(url)
    +        self.assertHttpStatus(response, 200)
    +        response_content = response.content.decode(response.charset)
    +        # The important thing here is that the data-field-parent and data-field-value are correctly quoted
    +        self.assertInHTML(
    +            """
    +<span class="filter-selection-choice-remove remove-filter-param"
    +      data-field-type="child"
    +      data-field-parent="location_type"
    +      data-field-value="1 onmouseover=alert(&#x27;hi&#x27;) foo=bar"
    +>×</span>""",  # noqa: RUF001 - ambiguous-unicode-character-string
    +            response_content,
    +        )
    +
     
     class ForceScriptNameTestcase(TestCase):
         """Basic test to assert that `settings.FORCE_SCRIPT_NAME` works as intended."""
    
42440ebd9b38

Fix quoting of query parameters in list view (#5646)

https://github.com/nautobot/nautobotGlenn MatthewsApr 30, 2024via ghsa
4 files changed · +41 14
  • changes/5646.security+1 0 added
    @@ -0,0 +1 @@
    +Fixed a reflected-XSS vulnerability ([GHSA-jxgr-gcj5-cqqg](https://github.com/nautobot/nautobot/security/advisories/GHSA-jxgr-gcj5-cqqg)) in object-list view rendering of user-provided query parameters.
    
  • nautobot/core/templates/generic/object_list.html+3 3 modified
    @@ -74,7 +74,7 @@ <h1>{% block title %}{{ title }}{% endblock %}</h1>
                         class="remove-filter-param"
                         title="Remove all items"
                         data-field-type="parent"
    -                    data-field-value={{ field.name }}
    +                    data-field-value="{{ field.name }}"
                 >×</span>
                 <ul class="filter-selection-rendered">
                     {% for value in field.values %}
    @@ -85,8 +85,8 @@ <h1>{% block title %}{{ title }}{% endblock %}</h1>
                         <span
                                 class="filter-selection-choice-remove remove-filter-param"
                                 data-field-type="child"
    -                            data-field-parent={{ field.name }}
    -                            data-field-value={{ value.name }}
    +                            data-field-parent="{{ field.name }}"
    +                            data-field-value="{{ value.name }}"
                         >×</span>{{ value.display }}
                     </li>
                     {% endfor %}
    
  • nautobot/core/tests/test_views.py+19 0 modified
    @@ -242,6 +242,25 @@ def test_filtering_on_custom_select_filter_field(self):
             self.assertInHTML(locations[0].name, response_content)
             self.assertInHTML(locations[1].name, response_content)
     
    +    def test_filtering_crafted_query_params(self):
    +        """Test for reflected-XSS vulnerability GHSA-jxgr-gcj5-cqqg."""
    +        self.add_permissions("dcim.view_location")
    +        query_param = "?location_type=1 onmouseover=alert('hi') foo=bar"
    +        url = reverse("dcim:location_list") + query_param
    +        response = self.client.get(url)
    +        self.assertHttpStatus(response, 200)
    +        response_content = response.content.decode(response.charset)
    +        # The important thing here is that the data-field-parent and data-field-value are correctly quoted
    +        self.assertInHTML(
    +            """
    +<span class="filter-selection-choice-remove remove-filter-param"
    +      data-field-type="child"
    +      data-field-parent="location_type"
    +      data-field-value="1 onmouseover=alert(&#x27;hi&#x27;) foo=bar"
    +>×</span>""",  # noqa: RUF001 - ambiguous-unicode-character-string
    +            response_content,
    +        )
    +
     
     class ForceScriptNameTestcase(TestCase):
         """Basic test to assert that `settings.FORCE_SCRIPT_NAME` works as intended."""
    
  • nautobot/project-static/js/forms.js+18 11 modified
    @@ -632,18 +632,19 @@ function initializeDynamicFilterForm(context){
     
         // Remove applied filters
         this_context.find(".remove-filter-param").on("click", function(){
    -        let query_params = location.search;
    +        let query_params = new URLSearchParams(location.search);
             let type = $(this).attr("data-field-type");
             let field_value = $(this).attr("data-field-value");
    -        let query_string = location.search.substr(1).split("&");
     
             if (type === "parent") {
    -            query_string = query_string.filter(item => item.search(field_value) < 0);
    +            // Remove all instances of this query param
    +            query_params.delete(field_value);
             } else {
    +            // Remove this specific instance of this query param
                 let parent = $(this).attr("data-field-parent");
    -            query_string = query_string.filter(item => item.search(parent + "=" + field_value) < 0)
    +            query_params.delete(parent, field_value);
             }
    -        location.replace("?" + query_string.join("&"))
    +        location.assign("?" + query_params);
         })
     
         // On submit of filter form
    @@ -657,12 +658,18 @@ function initializeDynamicFilterForm(context){
             q_field_phantom.val(q_field.val())
             dynamic_form.append(q_field_phantom);
     
    -        // Get the serialize data from the forms and filter out query_params which values are empty e.g ?sam=&dan=2 becomes dan=2
    -        let dynamic_filter_form_query = $("#dynamic-filter-form").serialize().split("&").filter(params => params.split("=")[1]?.length || 0 )
    -        let default_filter_form_query = $("#default-filter form").serialize().split("&").filter(params => params.split("=")[1]?.length || 0 )
    -        // Union Operation
    -        let search_query = [...new Set([...default_filter_form_query, ...dynamic_filter_form_query])].join("&")
    -        location.replace("?" + search_query)
    +        // Get the serialized data from the forms and:
    +        // 1) filter out query_params which values are empty e.g ?sam=&dan=2 becomes dan=2
    +        // 2) combine the two forms into a single set of data without duplicate entries
    +        let search_query = new URLSearchParams();
    +        let dynamic_query = new URLSearchParams(new FormData(document.getElementById("dynamic-filter-form")));
    +        dynamic_query.forEach((value, key) => { if (value != "") { search_query.append(key, value); }});
    +        let default_query = new URLSearchParams(new FormData(document.getElementById("default-filter").firstElementChild));
    +        default_query.forEach((value, key) => {
    +            if (value != "" && !search_query.has(key, value)) { search_query.append(key, value); }
    +        });
    +        $("#FilterForm_modal").modal("hide");
    +        location.assign("?" + search_query);
         })
     
         // On submit of filter search form
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

9

News mentions

0

No linked articles in our index yet.