CVE-2023-48705 (Python, published 2023-11-22T16:15Z)
Description: value: Nautobot is a Network Source of Truth and Network Automation Platform built as a web application All users of Nautobot versions earlier than 1.6.6 or 2.0.5 are potentially affected by a cross-site scripting vulnerability. Due to incorrect usage of Django's `mark_safe()` API when rendering certain types of user-authored content; including custom links, job buttons, and computed fields; it is possible that users with permission to create or edit these types of content could craft a malicious payload (such as JavaScript code) that would be executed when rendering pages containing this content. The maintainers have fixed the incorrect uses of `mark_safe()` (generally by replacing them with appropriate use of `format_html()` instead) to prevent such malicious data from being executed. Users on Nautobot 1.6.x LTM should upgrade to v1.6.6 and users on Nautobot 2.0.x should upgrade to v2.0.5. Appropriate object permissions can and should be applied to restrict which users are permitted to create or edit the aforementioned types of user-authored content. Other than that, there is no direct workaround available.
Severity: nan (CVSS v3 base score: 5.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/nautobot/nautobot
Files changed:
.github/workflows/ci_pullrequest.yml
changes/4832.changed
changes/4832.housekeeping
changes/4832.security
nautobot/core/settings.py
nautobot/core/tables.py
nautobot/core/templatetags/helpers.py
nautobot/core/utils/data.py
nautobot/core/views/generic.py
nautobot/core/views/mixins.py
nautobot/core/views/utils.py
nautobot/dcim/models/devices.py
nautobot/dcim/views.py
nautobot/extras/models/customfields.py
nautobot/extras/models/models.py
nautobot/extras/models/relationships.py
nautobot/extras/tables.py
nautobot/extras/templatetags/computed_fields.py
nautobot/extras/templatetags/custom_links.py
nautobot/extras/templatetags/job_buttons.py
nautobot/extras/templatetags/plugins.py
nautobot/extras/tests/test_customfields.py
nautobot/extras/tests/test_relationships.py
nautobot/extras/tests/test_views.py
nautobot/extras/views.py
nautobot/ipam/tables.py
nautobot/ipam/views.py
poetry.lock
pyproject.toml
scripts/git-hooks/pre-commit
tasks.py
Commit message: [2.0] Fix unsafe `mark_safe` use in various features (#4832)

* Audit all uses of mark_safe() and change to format_html() where appropriate

* Test updates and change fragment

* Add ruff for linting, add S308 disables to audited code

* Test fixes

* Update change fragments

* One more change fragment

---------

Vulnerable code:
        help_text=mark_safe(
from django.utils.html import escape, format_html
        return mark_safe('<input type="checkbox" class="toggle" title="Toggle all" />')
            return mark_safe(f'<span class="label label-{css_class}">{label}</span>')
        return mark_safe(f'<span class="label color-block" style="background-color: #{value}">&nbsp;</span>')
            return mark_safe(f'<a href="{url}">{value}</a>')
        template = ""
            for v in value:
                template += format_html('<span class="label label-default">{}</span> ', v)
        return mark_safe(template)
        template = ""
        else:
            # Handle Relationships on the many side.
            if self.relationship.has_many(self.peer_side):
                v = value[0]
                meta = type(v.get_peer(record))._meta
                name = meta.verbose_name_plural if len(value) > 1 else meta.verbose_name
                template += format_html(
                    '<a href="{}?relationship={}&{}_id={}">{} {}</a>',
                    reverse("extras:relationshipassociation_list"),
                    self.relationship.key,
                    self.side,
                    record.id,
                    len(value),
                    name,
                )
            # Handle Relationships on the one side.
            else:
                v = value[0]
                peer = v.get_peer(record)
                template += format_html('<a href="{}">{}</a>', peer.get_absolute_url(), peer)
        return mark_safe(template)
HTML_TRUE = '<span class="text-success"><i class="mdi mdi-check-bold" title="Yes"></i></span>'
HTML_FALSE = '<span class="text-danger"><i class="mdi mdi-close-thick" title="No"></i></span>'
HTML_NONE = '<span class="text-muted">&mdash;</span>'
    return mark_safe(HTML_NONE)
    return mark_safe(match.group(1) + format_html(' id="{}" ', id_str) + match.group(2))
        return mark_safe(HTML_NONE)
        return mark_safe(HTML_TRUE)
    return mark_safe(HTML_FALSE)
    return mark_safe(html)
    return template.render(context=context)
from django.utils.html import escape
from django.utils.safestring import mark_safe
                    mark_safe(f"Invalid filters were specified: {filterset.errors}"),
            msg = f'{msg} <a href="{obj.get_absolute_url()}">{escape(obj)}</a>'
            msg = f"{msg} {escape(obj)}"
        messages.success(request, mark_safe(msg))
                    mark_safe(f'Imported object: <a href="{obj.get_absolute_url()}">{obj}</a>'),
from django.utils.html import escape
from django.utils.safestring import mark_safe
                    mark_safe(f"Invalid filters were specified: {self.filterset.errors}"),
                msg = f'{msg} <a href="{obj.get_absolute_url()}">{escape(obj)}</a>'
                msg = f"{msg} { escape(obj)}"
            messages.success(request, mark_safe(msg))
from django.utils.html import escape
                    field_info["format"] = mark_safe("<code>true</code> or <code>false</code>")
                    field_info["format"] = mark_safe("<code>YYYY-MM-DD</code>")
                    field_info["format"] = mark_safe('<code>"value,value"</code>')
            field_info["format"] = mark_safe("<code>true</code> or <code>false</code>")
            field_info["format"] = mark_safe("<code>YYYY-MM-DD</code>")
            field_info["format"] = mark_safe(
                field_info["format"] = mark_safe('<code>"name,name"</code> or <code>"UUID,UUID"</code>')
                field_info["format"] = mark_safe('<code>"app_label.model,app_label.model"</code>')
                field_info["format"] = mark_safe('<code>"UUID,UUID"</code>')
                field_info["format"] = mark_safe("<code>app_label.model</code>")
                field_info["format"] = mark_safe("<code>UUID</code>")
            field_info["format"] = mark_safe('<code>"value,value"</code>')
    err_message = (
        f"Unable to delete <strong>{', '.join(str(obj) for obj in obj_list)}</strong>. "
        f"{protected_count} dependent objects were found: "
    dependent_objects = []
    for dependent in protected_objects[:50]:
        if hasattr(dependent, "get_absolute_url"):
            dependent_objects.append(f'<a href="{dependent.get_absolute_url()}">{escape(dependent)}</a>')
        else:
            dependent_objects.append(str(dependent))
    err_message += ", ".join(dependent_objects)
    messages.error(request, mark_safe(err_message))
from django.utils.functional import cached_property
from django.utils.functional import classproperty
from django.utils.safestring import mark_safe
                        "u_height": mark_safe(
                            f'Unable to set 0U height: Found <a href="{url}">{racked_instance_count} instances</a> already '
                            f"mounted within racks."
from django.utils.html import escape
from django.utils.safestring import mark_safe
                msg = f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>'
                messages.success(request, mark_safe(msg))
            msg = f"Unable to remove master device {escape(device)} from the virtual chassis."
            messages.error(request, mark_safe(msg))
from django.utils.safestring import mark_safe
                        message=mark_safe(f"Values must match this regex: <code>{self.validation_regex}</code>"),
        help_text="Jinja2 template code for link text. Reference the object as <code>{{ obj }}</code> such as <code>{{ obj.platform.name }}</code>. Links which render as empty text will not be displayed.",
        help_text="Jinja2 template code for link URL. Reference the object as <code>{{ obj }}</code> such as <code>{{ obj.platform.name }}</code>.",
from django.utils.safestring import mark_safe
                        hint = (
                            f"<a target='_blank' href='{add_url}'>Click here</a> to create "
                            f"a {required_model_meta.verbose_name}."
                error_message = mark_safe(
                    f"{name_plural[0].upper()}{name_plural[1:]} require "
                    f"{num_required_verbose} {required_model_meta.verbose_name}, but no "
                    f"{required_model_meta.verbose_name_plural} exist yet. {hint}"
from django.utils.safestring import mark_safe
            return mark_safe(render_markdown(record.description))
from django.utils.html import escape
from django.utils.safestring import mark_safe
    template_code = ""

    for label, value in fields.items():
        escaped_label = escape(label)
        template_code += f"""
            <tr>
                <td><span title="{escaped_label}">{escaped_label}</span></td>
                <td>{escape(value)}</td>
            <tr>
            """
    return mark_safe(template_code)
    template_code = ""
                    template_code += LINK_BUTTON.format(link_rendered, link_target, cl.button_class, text_rendered)
                template_code += (
                    f'<a class="btn btn-sm btn-default" disabled="disabled" title="{e}">'
                    f'<i class="mdi mdi-alert"></i> {cl.name}</a>\n'
        links_rendered = []
                    links_rendered.append(GROUP_LINK.format(link_rendered, link_target, text_rendered))
                links_rendered.append(
                    f'<li><a disabled="disabled" title="{e}"><span class="text-muted">'
                    f'<i class="mdi mdi-alert"></i> {cl.name}</span></a></li>'
            template_code += GROUP_BUTTON.format(links[0].button_class, group, "".join(links_rendered))
    return mark_safe(template_code)
    buttons_html = forms_html = ""
    hidden_inputs = HIDDEN_INPUTS.format(
                        buttons_html += CONFIRM_BUTTON.format(**template_args)
                        forms_html += CONFIRM_MODAL.format(**template_args)
                        buttons_html += NO_CONFIRM_BUTTON.format(**template_args)
                        forms_html += NO_CONFIRM_FORM.format(**template_args)
                buttons_html += (
                    f'<a class="btn btn-sm btn-default" disabled="disabled" title="{e}">'
                    f'<i class="mdi mdi-alert"></i> {jb.name}</a>\n'
        buttons_rendered = ""
                        buttons_rendered += "<li>" + CONFIRM_BUTTON.format(**template_args) + "</li>"
                        forms_html += CONFIRM_MODAL.format(**template_args)
                        buttons_rendered += "<li>" + NO_CONFIRM_BUTTON.format(**template_args) + "</li>"
                        forms_html += NO_CONFIRM_FORM.format(**template_args)
                buttons_rendered += (
                    f'<li><a disabled="disabled" title="{e}"><span class="text-muted">'
                    f'<i class="mdi mdi-alert"></i> {jb.name}</span></a></li>'
            buttons_html += GROUP_DROPDOWN.format(
    return mark_safe(buttons_html + forms_html)
    return mark_safe(html)
                '<span class="label label-default">Bar</span> <span class="label label-default">Baz</span> '
                        "objects_nonexistent": "Circuit types require a platform, but no platforms exist yet. "
                        "objects_nonexistent": "Circuit types require a platform, but no platforms exist yet.",
    ComputedField,
class CustomLinkTest(TestCase):
from django.utils.html import escape
from django.utils.safestring import mark_safe
                    msg = f'{msg} <a href="{obj.get_absolute_url()}">{escape(obj)}</a>'
                    msg = f"{msg} {escape(obj)}"
                messages.success(request, mark_safe(msg))
                    msg = f'{msg} <a href="{obj.get_absolute_url()}">{escape(obj)}</a>'
                    msg = f"{msg} {escape(obj)}"
                messages.success(request, mark_safe(msg))
        msg = f'Job enqueued. <a href="{result.get_absolute_url()}">Click here for the results.</a>'
        messages.info(request=request, message=mark_safe(msg))
                    msg = f'{msg} <a href="{obj.get_absolute_url()}">{escape(obj)}</a>'
                    msg = f"{msg} {escape(obj)}"
                messages.success(request, mark_safe(msg))
AVAILABLE_LABEL = mark_safe('<span class="label label-success">Available</span>')
from django.utils.html import escape
from django.utils.safestring import mark_safe
        warning_msg = (
            '<p>This <a href="'
            + static("docs/models/ipam/prefix.html")
            + '#prefix-hierarchy">will be considered invalid data</a> in a future release.</p>'
                mark_safe(
                    f'{obj} is a {obj.type.title()} prefix but its parent <a href="{obj.parent.get_absolute_url()}">'
                    f"{obj.parent}</a> is a {obj.parent.type.title()}. {warning_msg} "
                    f'Consider <a href="{edit_url}">changing the type of {obj}</a> and/or '
                    f'<a href="{parent_edit_url}">{obj.parent}</a> to resolve this issue.'
            children_link = '<a href="' + reverse("ipam:prefix_list") + f'?parent={obj.pk}">its children</a>'
                    mark_safe(
                        f"{obj} is a Container prefix and should not contain child prefixes of type Pool. "
                        f"{warning_msg} Consider creating an intermediary Network prefix, or changing "
                        f"the type of {children_link} to Network, to resolve this issue."
                    mark_safe(
                        f"{obj} is a Network prefix and should not contain child prefixes of types Container or "
                        f'Network. {warning_msg} Consider <a href="{edit_url}">changing the type of {obj}</a> '
                        f"to Container, or changing the type of {children_link} to Pool, to resolve this issue."
                    mark_safe(
                        f"{obj} is a Pool prefix and should not contain other prefixes. {warning_msg} "
                        f'Consider either <a href="{edit_url}">changing the type of {obj}</a> '
                        f"to Container or Network, or deleting {children_link}, to resolve this issue."
            ip_warning_msg = (
                '<p>This <a href="'
                + static("docs/models/ipam/ipaddress.html")
                + '#ipaddress-parenting-concrete-relationship">will be considered invalid data</a> '
                "in a future release.</p>"
                ip_link = '<a href="' + reverse("ipam:ipaddress_list") + f'?parent={obj.pk}">these IP addresses</a>'
                    mark_safe(
                        f"{obj} is a Container prefix and should not directly contain IP addresses. {ip_warning_msg} "
                        f'Consider either <a href="{edit_url}">changing the type of {obj}</a> to Network, or '
                        f'<a href="{create_url}">creating one or more child prefix(es) of type Network</a> to contain '
                        f"{ip_link}, to resolve this issue."
                    mark_safe(
                        f"{obj} is a Container prefix and should not directly contain IP addresses. {ip_warning_msg} "
                        f'Consider <a href="{edit_url}">changing the type of {obj}</a> to Network '
                        "to resolve this issue."
            warning_msg = (
                '<p>This <a href="'
                + static("docs/models/ipam/ipaddress.html")
                + '#ipaddress-parenting-concrete-relationship">will be considered invalid data</a> '
                "in a future release.</p>"
            parent_link = f'<a href="{obj.parent.get_absolute_url()}">{obj.parent}</a>'
                    mark_safe(
                        f"IP address {obj} currently has prefix {parent_link} as its parent, which is a Container. "
                        f'{warning_msg} Consider <a href="{create_url}">creating an intermediate /{obj.mask_length} '
                        "prefix of type Network</a> to resolve this issue."
                    mark_safe(
                        f"IP address {obj} currently has prefix {parent_link} as its parent, which is a Container. "
                        f'{warning_msg} Consider <a href="'
                        + reverse("ipam:prefix_edit", kwargs={"pk": obj.parent.pk})
                        + '">changing the prefix</a> to type Network or Pool to resolve this issue.'
                    msg = (
                        f"Merged {deleted_count} {self.queryset.model._meta.verbose_name} "
                        f'into <a href="{merged_ip.get_absolute_url()}">{escape(merged_ip)}</a>'
                    messages.success(request, mark_safe(msg))
content-hash = "5f9bca6f1bca37e007a5611ec280e9842d28d77bf72c68d4f90ff627fd569d00"

Fixed code:
  ruff:
    runs-on: "ubuntu-20.04"
    env:
      INVOKE_NAUTOBOT_LOCAL: "True"
    steps:
      - name: "Check out repository code"
        uses: "actions/checkout@v2"
      - name: "Setup environment"
        uses: "networktocode/gh-action-setup-poetry-environment@v2"
      - name: "Linting: ruff"
        run: "poetry run invoke ruff --output-format github"
      - "ruff"
      - "ruff"
      - "ruff"
      - "ruff"
      - "ruff"
      - "ruff"
Changed the `render_jinja2()` API to no longer automatically call `mark_safe()` on the output.
Added `ruff` to invoke tasks and CI.
Fixed cross-site-scripting (XSS) potential with maliciously crafted Custom Links, Computed Fields, and Job Buttons (GHSA-cf9f-wmhp-v4pr).
        help_text=mark_safe(  # noqa: S308
from django.utils.html import escape, format_html, format_html_join
        return mark_safe('<input type="checkbox" class="toggle" title="Toggle all" />')  # noqa: S308
            return format_html('<span class="label label-{}">{}</span>', css_class, label)
        return format_html('<span class="label color-block" style="background-color: #{}">&nbsp;</span>', value)
            return format_html('<a href="{}">{}</a>', url, value)
            template = format_html_join(" ", '<span class="label label-default">{}</span>', ((v,) for v in value))
        return template
        # Handle Relationships on the many side.
        if self.relationship.has_many(self.peer_side):
            v = value[0]
            meta = type(v.get_peer(record))._meta
            name = meta.verbose_name_plural if len(value) > 1 else meta.verbose_name
            return format_html(
                '<a href="{}?relationship={}&{}_id={}">{} {}</a>',
                reverse("extras:relationshipassociation_list"),
                self.relationship.key,
                self.side,
                record.id,
                len(value),
                name,
            )
        # Handle Relationships on the one side.
        else:
            v = value[0]
            peer = v.get_peer(record)
            return format_html('<a href="{}">{}</a>', peer.get_absolute_url(), peer)
HTML_TRUE = mark_safe('<span class="text-success"><i class="mdi mdi-check-bold" title="Yes"></i></span>')  # noqa: S308
HTML_FALSE = mark_safe('<span class="text-danger"><i class="mdi mdi-close-thick" title="No"></i></span>')  # noqa: S308
HTML_NONE = mark_safe('<span class="text-muted">&mdash;</span>')  # noqa: S308
    return HTML_NONE
    return mark_safe(match.group(1) + format_html(' id="{}" ', id_str) + match.group(2))  # noqa: S308
        return HTML_NONE
        return HTML_TRUE
    return HTML_FALSE
    return mark_safe(html)  # noqa: S308
    # For reasons unknown to me, django-jinja2 `template.render()` implicitly calls `mark_safe()` on the rendered text.
    # This is a security risk in general, especially so in our case because we're often using this function to render
    # a user-provided template and don't want to open ourselves up to script injection or similar issues.
    # There's no `mark_unsafe()` function, but concatenating a SafeString to an ordinary string (even "") suffices.
    return "" + template.render(context=context)
from django.utils.html import format_html
                    format_html("Invalid filters were specified: {}", filterset.errors),
            msg = format_html('{} <a href="{}">{}</a>', msg, obj.get_absolute_url(), obj)
            msg = format_html("{} {}", msg, obj)
        messages.success(request, msg)
                    format_html('Imported object: <a href="{}">{}</a>', obj.get_absolute_url(), obj),
from django.utils.html import format_html
                    format_html("Invalid filters were specified: {}", self.filterset.errors),
                msg = format_html('{} <a href="{}">{}</a>', msg, obj.get_absolute_url(), obj)
                msg = format_html("{} {}", msg, obj)
            messages.success(request, msg)
from django.utils.html import format_html, format_html_join
                    field_info["format"] = mark_safe("<code>true</code> or <code>false</code>")  # noqa: S308
                    field_info["format"] = mark_safe("<code>YYYY-MM-DD</code>")  # noqa: S308
                    field_info["format"] = mark_safe('<code>"value,value"</code>')  # noqa: S308
            field_info["format"] = mark_safe("<code>true</code> or <code>false</code>")  # noqa: S308
            field_info["format"] = mark_safe("<code>YYYY-MM-DD</code>")  # noqa: S308
            field_info["format"] = mark_safe(  # noqa: S308
                field_info["format"] = mark_safe('<code>"name,name"</code> or <code>"UUID,UUID"</code>')  # noqa: S308
                field_info["format"] = mark_safe('<code>"app_label.model,app_label.model"</code>')  # noqa: S308
                field_info["format"] = mark_safe('<code>"UUID,UUID"</code>')  # noqa: S308
                field_info["format"] = mark_safe("<code>app_label.model</code>")  # noqa: S308
                field_info["format"] = mark_safe("<code>UUID</code>")  # noqa: S308
            field_info["format"] = mark_safe('<code>"value,value"</code>')  # noqa: S308
    err_message = format_html(
        "Unable to delete <strong>{}</strong>. {} dependent objects were found: ",
        ", ".join(str(obj) for obj in obj_list),
        protected_count,
    err_message += format_html_join(
        ", ",
        '<a href="{}">{}</a>',
        ((dependent.get_absolute_url(), dependent) for dependent in protected_objects[:50]),
    )
    messages.error(request, err_message)
from django.utils.functional import cached_property, classproperty
from django.utils.html import format_html
                        "u_height": format_html(
                            "Unable to set 0U height: "
                            'Found <a href="{}">{} instances</a> already mounted within racks.',
                            url,
                            racked_instance_count,
from django.utils.html import format_html
                msg = format_html('Added member <a href="{}">{}</a>', device.get_absolute_url(), device)
                messages.success(request, msg)
            msg = format_html("Unable to remove master device {} from the virtual chassis.", device)
            messages.error(request, msg)
from django.utils.html import format_html
                        message=format_html("Values must match this regex: <code>{}</code>", self.validation_regex),
        help_text="Jinja2 template code for link text. "
        "Reference the object as <code>{{ obj }}</code> such as <code>{{ obj.platform.name }}</code>. "
        "Links which render as empty text will not be displayed.",
        help_text="Jinja2 template code for link URL. "
        "Reference the object as <code>{{ obj }}</code> such as <code>{{ obj.platform.name }}</code>.",
from django.utils.html import format_html
from nautobot.core.templatetags.helpers import bettertitle
                        hint = format_html(
                            '<a target="_blank" href="{}">Click here</a> to create a {}.',
                            add_url,
                            required_model_meta.verbose_name,
                error_message = format_html(
                    "{} require {} {}, but no {} exist yet. ",
                    bettertitle(name_plural),
                    num_required_verbose,
                    required_model_meta.verbose_name,
                    required_model_meta.verbose_name_plural,
                error_message += hint
            return render_markdown(record.description)
from django.utils.html import format_html_join
    return format_html_join(
        "\n",
        '<tr><td><span title="{}">{}</span></td><td>{}</td></tr>',
        ((label, label, value) for label, value in fields.items()),
    )
from django.utils.html import format_html
    template_code = mark_safe("")  # noqa: S308
                    template_code += format_html(
                        LINK_BUTTON, link_rendered, link_target, cl.button_class, text_rendered
                    )
                template_code += format_html(
                    '<a class="btn btn-sm btn-default" disabled="disabled" title="{}">'
                    '<i class="mdi mdi-alert"></i> {}</a>\n',
                    e,
                    cl.name,
        links_rendered = mark_safe("")  # noqa: S308
                    links_rendered += format_html(GROUP_LINK, link_rendered, link_target, text_rendered)
                links_rendered += format_html(
                    '<li><a disabled="disabled" title="{}"><span class="text-muted">'
                    '<i class="mdi mdi-alert"></i> {}</span></a></li>',
                    e,
                    cl.name,
            template_code += format_html(GROUP_BUTTON, links[0].button_class, group, links_rendered)
    return template_code
from django.utils.html import format_html
    buttons_html = forms_html = mark_safe("")  # noqa: S308
    hidden_inputs = format_html(
        HIDDEN_INPUTS,
                        buttons_html += format_html(CONFIRM_BUTTON, **template_args)
                        forms_html += format_html(CONFIRM_MODAL, **template_args)
                        buttons_html += format_html(NO_CONFIRM_BUTTON, **template_args)
                        forms_html += format_html(NO_CONFIRM_FORM, **template_args)
                buttons_html += format_html(
                    '<a class="btn btn-sm btn-default" disabled="disabled" title="{}">'
                    '<i class="mdi mdi-alert"></i> {}</a>\n',
                    e,
                    jb.name,
        buttons_rendered = mark_safe("")  # noqa: S308
                        buttons_rendered += (
                            mark_safe("<li>")  # noqa: S308
                            + format_html(CONFIRM_BUTTON, **template_args)
                            + mark_safe("</li>")  # noqa: S308
                        )
                        forms_html += format_html(CONFIRM_MODAL, **template_args)
                        buttons_rendered += (
                            mark_safe("<li>")  # noqa: S308
                            + format_html(NO_CONFIRM_BUTTON, **template_args)
                            + mark_safe("</li>")  # noqa: S308
                        )
                        forms_html += format_html(NO_CONFIRM_FORM, **template_args)
                buttons_rendered += format_html(
                    '<li><a disabled="disabled" title="{}"><span class="text-muted">'
                    '<i class="mdi mdi-alert"></i> {}</span></a></li>',
                    e,
                    jb.name,
            buttons_html += format_html(
                GROUP_DROPDOWN,
    return buttons_html + forms_html
    return mark_safe(html)  # noqa: S308
                '<span class="label label-default">Bar</span> <span class="label label-default">Baz</span>'
                        "objects_nonexistent": "Circuit Types require a platform, but no platforms exist yet. "
                        "objects_nonexistent": "Circuit Types require a platform, but no platforms exist yet.",
from django.utils.html import format_html
    ComputedField,
class ComputedFieldRenderingTestCase(TestCase):
    """Tests for the inclusion of ComputedFields, distinct from tests of the ComputedField views themselves."""

    user_permissions = ["dcim.view_locationtype"]

    def setUp(self):
        super().setUp()
        self.computedfield = ComputedField(
            content_type=ContentType.objects.get_for_model(LocationType),
            key="test",
            label="Computed Field",
            template="FOO {{ obj.name }} BAR",
            fallback_value="Fallback Value",
            weight=100,
        )
        self.computedfield.validated_save()
        self.location_type = LocationType.objects.get(name="Campus")

    def test_view_object_with_computed_field(self):
        """Ensure that the computed field template is rendered."""
        response = self.client.get(self.location_type.get_absolute_url(), follow=True)
        self.assertEqual(response.status_code, 200)
        content = extract_page_body(response.content.decode(response.charset))
        self.assertIn(f"FOO {self.location_type.name} BAR", content, content)

    def test_view_object_with_computed_field_fallback_value(self):
        """Ensure that the fallback_value is rendered if the template fails to render."""
        # Make the template invalid to demonstrate the fallback value
        self.computedfield.template = "FOO {{ obj."
        self.computedfield.validated_save()
        response = self.client.get(self.location_type.get_absolute_url(), follow=True)
        self.assertEqual(response.status_code, 200)
        content = extract_page_body(response.content.decode(response.charset))
        self.assertIn("Fallback Value", content, content)

    def test_view_object_with_computed_field_unsafe_template(self):
        """Ensure that computed field templates can't be used as an XSS vector."""
        self.computedfield.template = '<script>alert("Hello world!"</script>'
        self.computedfield.validated_save()
        response = self.client.get(self.location_type.get_absolute_url(), follow=True)
        self.assertEqual(response.status_code, 200)
        content = extract_page_body(response.content.decode(response.charset))
        self.assertNotIn("<script>alert", content, content)
        self.assertIn("&lt;script&gt;alert", content, content)

    def test_view_object_with_computed_field_unsafe_fallback_value(self):
        """Ensure that computed field fallback values can't be used as an XSS vector."""
        self.computedfield.template = "FOO {{ obj."
        self.computedfield.fallback_value = '<script>alert("Hello world!"</script>'
        self.computedfield.validated_save()
        response = self.client.get(self.location_type.get_absolute_url(), follow=True)
        self.assertEqual(response.status_code, 200)
        content = extract_page_body(response.content.decode(response.charset))
        self.assertNotIn("<script>alert", content, content)
        self.assertIn("&lt;script&gt;alert", content, content)


class CustomLinkRenderingTestCase(TestCase):
    """Tests for the inclusion of CustomLinks, distinct from tests of the CustomLink views themselves."""

    def test_view_object_with_unsafe_custom_link_text(self):
        """Ensure that custom links can't be used as a vector for injecting scripts or breaking HTML."""
        customlink = CustomLink(
            content_type=ContentType.objects.get_for_model(Location),
            name="Test",
            text='<script>alert("Hello world!")</script>',
            target_url="http://example.com/?location=None",
            new_window=False,
        )
        customlink.validated_save()
        location_type = LocationType.objects.get(name="Campus")
        status = Status.objects.get_for_model(Location).first()
        location = Location(name="Test Location", location_type=location_type, status=status)
        location.save()

        response = self.client.get(location.get_absolute_url(), follow=True)
        self.assertEqual(response.status_code, 200)
        content = extract_page_body(response.content.decode(response.charset))
        self.assertNotIn("<script>alert", content, content)
        self.assertIn("&lt;script&gt;alert", content, content)
        self.assertIn(format_html('<a href="{}"', customlink.target_url), content, content)

    def test_view_object_with_unsafe_custom_link_url(self):
        """Ensure that custom links can't be used as a vector for injecting scripts or breaking HTML."""
        customlink = CustomLink(
            content_type=ContentType.objects.get_for_model(Location),
            name="Test",
            text="Hello",
            target_url='"><script>alert("Hello world!")</script><a href="',
            new_window=False,
        )
        customlink.validated_save()
        location_type = LocationType.objects.get(name="Campus")
        status = Status.objects.get_for_model(Location).first()
        location = Location(name="Test Location", location_type=location_type, status=status)
        location.save()

        response = self.client.get(location.get_absolute_url(), follow=True)
        self.assertEqual(response.status_code, 200)
        content = extract_page_body(response.content.decode(response.charset))
        self.assertNotIn("<script>alert", content, content)
        self.assertIn("&lt;script&gt;alert", content, content)
        self.assertIn(format_html('<a href="{}"', customlink.target_url), content, content)

    def test_view_object_with_unsafe_custom_link_name(self):
        """Ensure that custom links can't be used as a vector for injecting scripts or breaking HTML."""
        customlink = CustomLink(
            content_type=ContentType.objects.get_for_model(Location),
            name='<script>alert("Hello World")</script>',
            text="Hello",
            target_url="http://example.com/?location={{ obj.name ",  # intentionally bad jinja2 to trigger error case
            new_window=False,
        )
        customlink.validated_save()
        location_type = LocationType.objects.get(name="Campus")
        status = Status.objects.get_for_model(Location).first()
        location = Location(name="Test Location", location_type=location_type, status=status)
        location.save()

        response = self.client.get(location.get_absolute_url(), follow=True)
        self.assertEqual(response.status_code, 200)
        content = extract_page_body(response.content.decode(response.charset))
        self.assertNotIn("<script>alert", content, content)
        self.assertIn("&lt;script&gt;alert", content, content)

class JobButtonRenderingTestCase(TestCase):
    """Tests for the rendering of JobButtons, distinct from tests of the JobButton views themselves."""

    user_permissions = ["dcim.view_locationtype"]

    def setUp(self):
        super().setUp()
        self.job_button = JobButton(
            name="JobButton",
            text="JobButton {{ obj.name }}",
            job=Job.objects.get(job_class_name="TestJobButtonReceiverSimple"),
            confirmation=False,
        )
        self.job_button.validated_save()
        self.job_button.content_types.add(ContentType.objects.get_for_model(LocationType))
        self.location_type = LocationType.objects.get(name="Campus")

    def test_view_object_with_job_button(self):
        """Ensure that the job button is rendered."""
        response = self.client.get(self.location_type.get_absolute_url(), follow=True)
        self.assertEqual(response.status_code, 200)
        content = extract_page_body(response.content.decode(response.charset))
        self.assertIn(f"JobButton {self.location_type.name}", content, content)

    def test_view_object_with_unsafe_text(self):
        """Ensure that JobButton text can't be used as a vector for XSS."""
        self.job_button.text = '<script>alert("Hello world!")</script>'
        self.job_button.validated_save()
        response = self.client.get(self.location_type.get_absolute_url(), follow=True)
        self.assertEqual(response.status_code, 200)
        content = extract_page_body(response.content.decode(response.charset))
        self.assertNotIn("<script>alert", content, content)
        self.assertIn("&lt;script&gt;alert", content, content)

        # Make sure grouped rendering is safe too
        self.job_button.group = '<script>alert("Goodbye")</script>'
        self.job_button.validated_save()
        response = self.client.get(self.location_type.get_absolute_url(), follow=True)
        self.assertEqual(response.status_code, 200)
        content = extract_page_body(response.content.decode(response.charset))
        self.assertNotIn("<script>alert", content, content)
        self.assertIn("&lt;script&gt;alert", content, content)

    def test_view_object_with_unsafe_name(self):
        """Ensure that JobButton names can't be used as a vector for XSS."""
        self.job_button.text = "JobButton {{ obj"
        self.job_button.name = '<script>alert("Yo")</script>'
        self.job_button.validated_save()
        response = self.client.get(self.location_type.get_absolute_url(), follow=True)
        self.assertEqual(response.status_code, 200)
        content = extract_page_body(response.content.decode(response.charset))
        self.assertNotIn("<script>alert", content, content)
        self.assertIn("&lt;script&gt;alert", content, content)


from django.utils.html import format_html
                    msg = format_html('{} <a href="{}">{}</a>', msg, obj.get_absolute_url(), obj)
                    msg = format_html("{} {}", msg, obj)
                messages.success(request, msg)
                    msg = format_html('{} <a href="{}">{}</a>', msg, obj.get_absolute_url(), obj)
                    msg = format_html("{} {}", msg, obj)
                messages.success(request, msg)
        msg = format_html('Job enqueued. <a href="{}">Click here for the results.</a>', result.get_absolute_url())
        messages.info(request=request, message=msg)
                    msg = format_html('{} <a href="{}">{}</a>', msg, obj.get_absolute_url(), obj)
                    msg = format_html("{} {}", msg, obj)
                messages.success(request, msg)
AVAILABLE_LABEL = mark_safe('<span class="label label-success">Available</span>')  # noqa: S308
from django.utils.html import format_html
        warning_msg = format_html(
            '<p>This <a href="{}#prefix-hierarchy">will be considered invalid data</a> in a future release.</p>',
            static("docs/models/ipam/prefix.html"),
                format_html(
                    '{} is a {} prefix but its parent <a href="{}">{}</a> is a {}. {} Consider '
                    '<a href="{}">changing the type of {}</a> and/or <a href="{}">{}</a> to resolve this issue.',
                    obj,
                    obj.type.title(),
                    obj.parent.get_absolute_url(),
                    obj.parent,
                    obj.parent.type.title(),
                    warning_msg,
                    edit_url,
                    obj,
                    parent_edit_url,
                    obj.parent,
            children_link = format_html('<a href="{}?parent={}">its children</a>', reverse("ipam:prefix_list"), obj.pk)
                    format_html(
                        "{} is a Container prefix and should not contain child prefixes of type Pool. {} "
                        "Consider creating an intermediary Network prefix, or changing the type of {} to Network, "
                        "to resolve this issue.",
                        obj,
                        warning_msg,
                        children_link,
                    format_html(
                        "{} is a Network prefix and should not contain child prefixes of types Container or Network. "
                        '{} Consider <a href="{}">changing the type of {}</a> to Container, '
                        "or changing the type of {} to Pool, to resolve this issue.",
                        obj,
                        warning_msg,
                        edit_url,
                        obj,
                        children_link,
                    format_html(
                        "{} is a Pool prefix and should not contain other prefixes. {} "
                        'Consider either <a href="{}">changing the type of {}</a> to Container or Network, '
                        "or deleting {}, to resolve this issue.",
                        obj,
                        warning_msg,
                        edit_url,
                        obj,
                        children_link,
            ip_warning_msg = format_html(
                '<p>This <a href="{}#ipaddress-parenting-concrete-relationship">will be considered invalid data</a> '
                "in a future release.</p>",
                static("docs/models/ipam/ipaddress.html"),
                ip_link = format_html(
                    '<a href="{}?parent={}">these IP addresses</a>', reverse("ipam:ipaddress_list"), obj.pk
                )
                    format_html(
                        "{} is a Container prefix and should not directly contain IP addresses. {} "
                        'Consider either <a href="{}">changing the type of {}</a> to Network, '
                        'or <a href="{}">creating one or more child prefix(es) of type Network</a> to contain {}, '
                        "to resolve this issue.",
                        obj,
                        ip_warning_msg,
                        edit_url,
                        obj,
                        create_url,
                        ip_link,
                    format_html(
                        "{} is a Container prefix and should not directly contain IP addresses. {} "
                        'Consider <a href="{}">changing the type of {}</a> to Network to resolve this issue.',
                        obj,
                        ip_warning_msg,
                        edit_url,
                        obj,
            warning_msg = format_html(
                '<p>This <a href="{}#ipaddress-parenting-concrete-relationship">will be considered invalid data</a> '
                "in a future release.</p>",
                static("docs/models/ipam/ipaddress.html"),
            parent_link = format_html('<a href="{}">{}</a>', obj.parent.get_absolute_url(), obj.parent)
                    format_html(
                        "IP address {} currently has prefix {} as its parent, which is a Container. {} "
                        'Consider <a href="{}">creating an intermediate /{} prefix of type Network</a> '
                        "to resolve this issue.",
                        obj,
                        parent_link,
                        warning_msg,
                        create_url,
                        obj.mask_length,
                    format_html(
                        "IP address {} currently has prefix {} as its parent, which is a Container. {} "
                        'Consider <a href="{}">changing the prefix</a> to type Network or Pool to resolve this issue.',
                        obj,
                        parent_link,
                        warning_msg,
                        reverse("ipam:prefix_edit", kwargs={"pk": obj.parent.pk}),
                    msg = format_html(
                        'Merged {} {} into <a href="{}">{}</a>',
                        deleted_count,
                        self.queryset.model._meta.verbose_name,
                        merged_ip.get_absolute_url(),
                        merged_ip,
                    messages.success(request, msg)
[[package]]
name = "ruff"
version = "0.1.6"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
    {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"},
    {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"},
    {file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"},
    {file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"},
    {file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"},
    {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"},
    {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"},
    {file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"},
    {file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"},
    {file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"},
    {file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"},
    {file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"},
    {file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"},
    {file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"},
    {file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"},
    {file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"},
    {file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"},
]

content-hash = "4709a608499593b8cdb5a9b38b34544891123dc1d86885de7a751c901ab07c4b"
# Combination linter and code formatter
ruff = "~0.1.6"
[tool.ruff]
line-length = 120
target-version = "py38"

[tool.ruff.lint]
select = [
    "E",  # pycodestyle
    "F",  # pyflakes
    # "I",  # isort
    "S308",  # flake8-bandit: suspicious-mark-safe-usage
    "W",  # pycodestyle
]
ignore = [
    "E501",  # pycodestyle: line-too-long
]

[tool.ruff.lint.isort]
lines-after-imports = 2
force-sort-within-sections = true

echo "Check static analysis via ruff..."
invoke ruff || EXIT=1

@task
def ruff(context, output_format="text"):
    """Run ruff to perform static analysis and linting."""
    command = f"ruff --output-format {output_format} development/ examples/ nautobot/ tasks.py"
    run_command(context, command)


    ruff(context)