CVE-2024-34707 (HTML, published 2024-05-14T15:39Z)
Description: value: Nautobot is a Network Source of Truth and Network Automation Platform. A Nautobot user with admin privileges can modify the `BANNER_TOP`, `BANNER_BOTTOM`, and `BANNER_LOGIN` configuration settings via the `/admin/constance/config/` endpoint. Normally these settings are used to provide custom banner text at the top and bottom of all Nautobot web pages (or specifically on the login page in the case of `BANNER_LOGIN`) but it was reported that an admin user can make use of these settings to inject arbitrary HTML, potentially exposing Nautobot users to security issues such as cross-site scripting (stored XSS). The vulnerability is fixed in Nautobot 1.6.22 and 2.2.4.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/nautobot/nautobot
Files changed:
changes/1858.added
changes/1858.security
nautobot/core/settings.py
nautobot/core/settings.yaml
nautobot/core/templates/admin/base.html
nautobot/core/templates/base_django.html
nautobot/core/templates/inc/javascript.html
nautobot/core/templates/inc/media.html
nautobot/core/templates/login.html
nautobot/core/templates/nautobot_config.py.j2
nautobot/core/tests/test_views.py
Commit message: Add 'javascript' and 'css' to BRANDING_FILEPATHS options. Sanitize BANNER_ content (#5697)

* Add 'javascript' and 'css' to BRANDING_FILEPATHS options. Address GHSA-r2hr-4v48-fjv3

Vulnerable code:
# Banners to display to users. HTML is allowed.
        help_text="Custom HTML to display in a banner at the bottom of all pages.",
        help_text="Custom HTML to display in a banner at the top of the login page.",
        help_text="Custom HTML to display in a banner at the top of all pages.",
    description: "Custom content to be displayed in a banner at the bottom of the page. HTML is allowed."
    description: "Custom content to be displayed on the login page above the login form. HTML is allowed."
    description: "Custom content to be displayed in a banner at the top of the page. HTML is allowed."
      A set of filepaths relative to the [`MEDIA_ROOT`](#media_root) which locate image assets used for
      custom branding. Each of these assets takes the place of the corresponding stock Nautobot asset.
      If a custom image asset is not provided for any of the above options, the stock Nautobot asset is used.
        default: ""
        default: ""
        default: ""
        default: ""
        default: ""
        default: ""
        default: ""
        default: ""
        default: ""
                {{ "BANNER_TOP"|settings_or_config|safe }}
                {{ "BANNER_BOTTOM"|settings_or_config|safe }}
                    {{ "BANNER_TOP"|settings_or_config|safe }}
                    {{ "BANNER_BOTTOM"|settings_or_config|safe }}
            <div style="margin-bottom: 25px">
                {{ "BANNER_LOGIN"|settings_or_config|safe }}

Fixed code:
Added support in `BRANDING_FILEPATHS` configuration to specify a custom `css` and/or `javascript` file to be added to Nautobot page content.
Added Markdown support to the `BANNER_TOP`, `BANNER_BOTTOM`, and `BANNER_LOGIN` configuration settings.
Added sanitization of HTML tags in the content of `BANNER_TOP`, `BANNER_BOTTOM`, and `BANNER_LOGIN` configuration to prevent against potential injection of malicious scripts (stored XSS) via these features ([GHSA-r2hr-4v48-fjv3](https://github.com/nautobot/nautobot/security/advisories/GHSA-r2hr-4v48-fjv3)).
# Banners to display to users. Markdown and limited HTML are allowed.
        help_text="Custom Markdown or limited HTML to display in a banner at the bottom of all pages.",
        help_text="Custom Markdown or limited HTML to display in a banner at the top of the login page.",
        help_text="Custom Markdown or limited HTML to display in a banner at the top of all pages.",
    "css": os.getenv("NAUTOBOT_BRANDING_FILEPATHS_CSS", None),  # Custom global CSS
    "javascript": os.getenv("NAUTOBOT_BRANDING_FILEPATHS_JAVASCRIPT", None),  # Custom global JavaScript
    description: >-
      Custom content to be displayed in a banner at the bottom of all Nautobot pages.
    details: |-
      +/- 2.2.4
          Markdown formatting is supported within this message, as well as
          [a limited subset of HTML](../../platform-functionality/template-filters.md#render_markdown).
    description: >-
      Custom content to be displayed in a banner on the login page above the login form.
    details: |-
      +/- 2.2.4
          Markdown formatting is supported within this message, as well as
          [a limited subset of HTML](../../platform-functionality/template-filters.md#render_markdown).
    description: >-
      Custom content to be displayed in a banner at the top of all Nautobot pages.
    details: |-
      +/- 2.2.4
          Markdown formatting is supported within this message, as well as
          [a limited subset of HTML](../../platform-functionality/template-filters.md#render_markdown).
      css: null
      javascript: null
      A set of filepaths relative to the [`MEDIA_ROOT`](#media_root) which locate assets used for
      custom branding of your Nautobot instance.
      With the exception of `css` and `javascript`, which provide the option to add an _additional_ file to Nautobot
      page content, each of the other assets takes the place of the corresponding stock Nautobot asset.
      If a custom asset is not provided for any of the above options, the stock Nautobot asset is used.
    details: |-
      +++ 2.1.0
          The `header_bullet` and `nav_bullet` assets were added as options.

      +++ 2.2.4
          The `css` and `javascript` assets were added as options.
      css:
        "$ref": "#/definitions/relative_path"
        default: null
        description: "Custom global CSS file"
        environment_variable: "NAUTOBOT_BRANDING_FILEPATHS_CSS"
        default: null
        default: null
        default: null
        default: null
        default: null
        default: null
        default: null
      javascript:
        "$ref": "#/definitions/relative_path"
        default: null
        description: "Custom global JavaScript file"
        environment_variable: "NAUTOBOT_BRANDING_FILEPATHS_JAVASCRIPT"
        default: null
        default: null
                {{ "BANNER_TOP"|settings_or_config|render_markdown }}
                {{ "BANNER_BOTTOM"|settings_or_config|render_markdown }}
                    {{ "BANNER_TOP"|settings_or_config|render_markdown }}
                    {{ "BANNER_BOTTOM"|settings_or_config|render_markdown }}
{% if settings.BRANDING_FILEPATHS.javascript %}
<script src="{% custom_branding_or_static 'javascript' None %}"></script>
{% endif %}
    {% if settings.BRANDING_FILEPATHS.css %}
    <link rel="stylesheet" id="custom-css" href="{% custom_branding_or_static 'css' None %}">
    {% endif %}
            <div class="alert alert-info text-center" role="alert">
                {{ "BANNER_LOGIN"|settings_or_config|render_markdown }}
#     "css": os.getenv("NAUTOBOT_BRANDING_FILEPATHS_CSS", None),  # Custom global CSS
#     "javascript": os.getenv("NAUTOBOT_BRANDING_FILEPATHS_JAVASCRIPT", None),  # Custom global JavaScript
    def test_banners_markdown(self):
        url = reverse("home")
        with override_settings(
            BANNER_TOP="# Hello world",
            BANNER_BOTTOM="[info](https://nautobot.com)",
        ):
            response = self.client.get(url)
        self.assertInHTML("<h1>Hello world</h1>", response.content.decode(response.charset))
        self.assertInHTML(
            '<a href="https://nautobot.com" rel="noopener noreferrer">info</a>',
            response.content.decode(response.charset),
        )

        with override_settings(BANNER_LOGIN="_Welcome to Nautobot!_"):
            self.client.logout()
            response = self.client.get(reverse("login"))
        self.assertInHTML("<em>Welcome to Nautobot!</em>", response.content.decode(response.charset))

    def test_banners_no_xss(self):
        url = reverse("home")
        with override_settings(
            BANNER_TOP='<script>alert("Hello from above!");</script>',
            BANNER_BOTTOM='<script>alert("Hello from below!");</script>',
        ):
            response = self.client.get(url)
        self.assertNotIn("Hello from above", response.content.decode(response.charset))
        self.assertNotIn("Hello from below", response.content.decode(response.charset))

        with override_settings(BANNER_LOGIN='<script>alert("Welcome to Nautobot!");</script>'):
            self.client.logout()
            response = self.client.get(reverse("login"))
        self.assertNotIn("Welcome to Nautobot!", response.content.decode(response.charset))