Nautobot has XSS potential in rendered Markdown fields
Description
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.10 or 2.1.2 are potentially impacted by a cross-site scripting vulnerability. Due to inadequate input sanitization, any user-editable fields that support Markdown rendering, including are potentially susceptible to cross-site scripting (XSS) attacks via maliciously crafted data. This issue is fixed in Nautobot versions 1.6.10 and 2.1.2.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Nautobot versions before 1.6.10 and 2.1.2 have a stored XSS vulnerability due to insufficient sanitization of Markdown-rendered fields.
Vulnerability
Overview
Nautobot versions prior to 1.6.10 and 2.1.2 are vulnerable to stored cross-site scripting (XSS) due to insufficient input sanitization in the render_markdown() utility function [1][2]. This function processes user-editable fields that support Markdown rendering, such as comments, descriptions, notes, and job log entries, allowing malicious HTML to be injected and executed when the content is viewed [3].
Attack
Vector
An attacker with the ability to edit any of these Markdown-supporting fields can craft malicious input containing HTML or JavaScript. When other users load the page containing the rendered content, the injected script executes in the context of the victim's browser. No special privileges beyond the ability to edit the field are required, making this a stored XSS vulnerability [2][3].
Impact
Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of the Nautobot application, potentially leading to session hijacking, credential theft, or unauthorized actions on behalf of the victim [3].
Mitigation
The vulnerability is fixed in Nautobot versions 1.6.10 and 2.1.2. The fix introduces the nh3 HTML sanitization library to sanitize the output of render_markdown(), stripping dangerous HTML tags and attributes while allowing a safe subset [1][2]. Users should upgrade to the latest patched versions. No workarounds or CVSS score have been provided at this time [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.
| Package | Affected versions | Patched versions |
|---|---|---|
nautobotPyPI | >= 2.0.0, < 2.1.2 | 2.1.2 |
nautobotPyPI | < 1.6.10 | 1.6.10 |
Affected products
2- nautobot/nautobotv5Range: >= 2.0.0, < 2.1.2
Patches
264312a4297b5[1.6] Sanitize render_markdown() output with nh3 library (#5134)
18 files changed · +221 −45
changes/5134.added+1 −0 added@@ -0,0 +1 @@ +Enhanced Markdown-supporting fields (`comments`, `description`, Notes, Job log entries, etc.) to also permit the use of a limited subset of "safe" HTML tags and attributes.
changes/5134.dependencies+1 −0 added@@ -0,0 +1 @@ +Added `nh3` HTML sanitization library as a dependency.
changes/5134.security+1 −0 added@@ -0,0 +1 @@ +Fixed an XSS vulnerability ([GHSA-v4xv-795h-rv4h](https://github.com/nautobot/nautobot/security/advisories/GHSA-v4xv-795h-rv4h)) in the `render_markdown()` utility function used to render comments, notes, job log entries, etc.
nautobot/core/settings.py+2 −1 modified@@ -671,7 +671,8 @@ ], "SUPPORT_MESSAGE": [ "", - "Help message to include on 4xx and 5xx error pages. Markdown is supported.\n" + "Help message to include on 4xx and 5xx error pages. " + "Markdown is supported, as are some HTML tags and attributes.\n" "If unspecified, instructions to join Network to Code's Slack community will be provided.", ], }
nautobot/core/tests/test_views.py+7 −7 modified@@ -370,9 +370,9 @@ def test_404_default_support_message(self): self.assertContains(response, "Network to Code", status_code=404) response_content = response.content.decode(response.charset) self.assertInHTML( - "If further assistance is required, please join the <code>#nautobot</code> channel " - 'on <a href="https://slack.networktocode.com/">Network to Code\'s Slack community</a> ' - "and post your question.", + "If further assistance is required, please join the <code>#nautobot</code> channel on " + '<a href="https://slack.networktocode.com/" rel="noopener noreferrer">Network to Code\'s ' + "Slack community</a> and post your question.", response_content, ) @@ -396,16 +396,16 @@ def test_500_default_support_message(self, mock_get): self.assertContains(response, "Network to Code", status_code=500) response_content = response.content.decode(response.charset) self.assertInHTML( - "If further assistance is required, please join the <code>#nautobot</code> channel " - 'on <a href="https://slack.networktocode.com/">Network to Code\'s Slack community</a> ' - "and post your question.", + "If further assistance is required, please join the <code>#nautobot</code> channel on " + '<a href="https://slack.networktocode.com/" rel="noopener noreferrer">Network to Code\'s ' + "Slack community</a> and post your question.", response_content, ) @override_settings(DEBUG=False, SUPPORT_MESSAGE="Hello world!") @mock.patch("nautobot.core.views.HomeView.get", side_effect=Exception) def test_500_custom_support_message(self, mock_get): - """Nautobot's custom 500 page should be used and should include a default support message.""" + """Nautobot's custom 500 page should be used and should include a custom support message if defined.""" url = reverse("home") with self.assertTemplateUsed("500.html"): self.client.raise_request_exception = False
nautobot/docs/additional-features/jobs.md+2 −2 modified@@ -90,7 +90,7 @@ This is the human-friendly name of your job, as will be displayed in the Nautobo #### `description` An optional human-friendly description of what this job does. -This can accept either plain text or Markdown-formatted text. It can also be multiple lines: +This can accept either plain text, Markdown-formatted text, or [a limited subset of HTML](template-filters.md#render_markdown). It can also be multiple lines: ```python class ExampleJob(Job): @@ -466,7 +466,7 @@ Messages recorded with `log()` or `log_debug()` will appear in a job's results b It is advised to log a message for each object that is evaluated so that the results will reflect how many objects are being manipulated or reported on. -Markdown rendering is supported for log messages. +Markdown rendering is supported for log messages, as well as [a limited subset of HTML](template-filters.md#render_markdown). +/- 1.3.4 As a security measure, the `message` passed to any of these methods will be passed through the `nautobot.utilities.logging.sanitize()` function in an attempt to strip out information such as usernames/passwords that should not be saved to the logs. This is of course best-effort only, and Job authors should take pains to ensure that such information is not passed to the logging APIs in the first place. The set of redaction rules used by the `sanitize()` function can be configured as [settings.SANITIZER_PATTERNS](../configuration/optional-settings.md#sanitizer_patterns).
nautobot/docs/additional-features/template-filters.md+75 −1 modified@@ -207,12 +207,86 @@ Render a dictionary as formatted JSON. ### render_markdown -Render text as Markdown. +Render and sanitize Markdown text into HTML. A limited subset of HTML tags and attributes are permitted in the text as well; non-permitted HTML will be stripped from the output for security. ```django {{ text | render_markdown }} ``` +#### Permitted HTML Tags and Attributes + ++++ 1.6.10 + +The set of permitted HTML tags is defined in `nautobot.core.constants.HTML_ALLOWED_TAGS`, and their permitted attributes are defined in `nautobot.core.constants.HTML_ALLOWED_ATTRIBUTES`. As of Nautobot 1.6.10 the following are permitted: + +??? info "Full list of HTML tags and attributes" + | Tag | Attributes | + | -------------- | -------------------------------------------------------------------- | + | `<a>` | `href`, `hreflang` | + | `<abbr>` | | + | `<acronym>` | | + | `<b>` | | + | `<bdi>` | | + | `<bdo>` | `dir` | + | `<blockquote>` | `cite` | + | `<br>` | | + | `<caption>` | | + | `<center>` | | + | `<cite>` | | + | `<code>` | | + | `<col>` | `align`, `char`, `charoff`, `span` | + | `<colgroup>` | `align`, `char`, `charoff`, `span` | + | `<dd>` | | + | `<del>` | `cite`, `datetime` | + | `<details>` | | + | `<div>` | | + | `<dl>` | | + | `<dt>` | | + | `<em>` | | + | `<h1>` | | + | `<h2>` | | + | `<h3>` | | + | `<h4>` | | + | `<h5>` | | + | `<h6>` | | + | `<hgroup>` | | + | `<hr>` | `align`, `size`, `width` | + | `<i>` | | + | `<img>` | `align`, `alt`, `height`, `src`, `width` | + | `<ins>` | `cite`, `datetime` | + | `<kbd>` | | + | `<li>` | | + | `<mark>` | | + | `<ol>` | `start` | + | `<p>` | | + | `<pre>` | | + | `<q>` | `cite` | + | `<rp>` | | + | `<rt>` | | + | `<rtc>` | | + | `<ruby>` | | + | `<s>` | | + | `<samp>` | | + | `<small>` | | + | `<span>` | | + | `<strike>` | | + | `<strong>` | | + | `<sub>` | | + | `<summary>` | | + | `<sup>` | | + | `<table>` | `align`, `char`, `charoff`, `summary` | + | `<tbody>` | `align`, `char`, `charoff` | + | `<td>` | `align`, `char`, `charoff`, `colspan`, `headers`, `rowspan` | + | `<th>` | `align`, `char`, `charoff`, `colspan`, `headers`, `rowspan`, `scope` | + | `<thead>` | `align`, `char`, `charoff` | + | `<time>` | | + | `<tr>` | `align`, `char`, `charoff` | + | `<tt>` | | + | `<u>` | | + | `<ul>` | | + | `<var>` | | + | `<wbr>` | | + ### render_yaml Render a dictionary as formatted YAML.
nautobot/docs/configuration/optional-settings.md+1 −1 modified@@ -901,7 +901,7 @@ If set to `False`, unknown/unrecognized filter parameters will be discarded and Default: `""` -A message to include on error pages (status code 403, 404, 500, etc.) when an error occurs. You can configure this to direct users to the appropriate contact(s) within your organization that provide support for Nautobot. Markdown formatting is supported within this message (raw HTML is not). +A message to include on error pages (status code 403, 404, 500, etc.) when an error occurs. You can configure this to direct users to the appropriate contact(s) within your organization that provide support for Nautobot. Markdown formatting is supported within this message, as well as [a limited subset of HTML](../additional-features/template-filters.md#render_markdown). If unset, the default message that will appear is `If further assistance is required, please join the #nautobot channel on [Network to Code's Slack community](https://slack.networktocode.com) and post your question.`
nautobot/docs/models/extras/note.md+1 −1 modified@@ -4,4 +4,4 @@ Notes provide a place for you to store notes or general information on an object, such as a Device, that may not require a specific field for. This could be a note on a recent upgrade, a warning about a problematic device, or the reason the Rack was marked with the Status `Retired`. -The note field supports [Markdown Basic Syntax](https://www.markdownguide.org/cheat-sheet/#basic-syntax). +The note field supports [Markdown Basic Syntax](https://www.markdownguide.org/cheat-sheet/#basic-syntax) as well as [a limited subset of HTML](../../additional-features/template-filters.md#render_markdown).
nautobot/extras/forms/forms.py+9 −5 modified@@ -360,18 +360,22 @@ class ConfigContextSchemaFilterForm(BootstrapMixin, forms.Form): ) +class CustomFieldDescriptionField(CommentField): + @property + def default_helptext(self): + return "Also used as the help text when editing models using this custom field.<br>" + super().default_helptext + + class CustomFieldForm(BootstrapMixin, forms.ModelForm): label = forms.CharField(required=True, max_length=50, help_text="Name of the field as displayed to users.") slug = SlugField( max_length=50, slug_source="label", help_text="Internal name of this field. Please use underscores rather than dashes.", ) - description = forms.CharField( + description = CustomFieldDescriptionField( + label="Description", required=False, - help_text="Also used as the help text when editing models using this custom field.<br>" - '<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">' - "Markdown</a> syntax is supported.", ) content_types = MultipleContentTypeField( feature="custom_fields", help_text="The object(s) to which this field applies." @@ -1083,7 +1087,7 @@ class JobButtonFilterForm(BootstrapMixin, forms.Form): class NoteForm(BootstrapMixin, forms.ModelForm): - note = CommentField + note = CommentField() class Meta: model = Note
nautobot/extras/models/jobs.py+4 −1 modified@@ -140,7 +140,10 @@ class Job(PrimaryModel): help_text="Human-readable name of this job", db_index=True, ) - description = models.TextField(blank=True, help_text="Markdown formatting is supported") + description = models.TextField( + blank=True, + help_text="Markdown formatting and a limited subset of HTML are supported", + ) # Control flags installed = models.BooleanField(
nautobot/utilities/constants.py+33 −0 modified@@ -1,3 +1,7 @@ +from copy import deepcopy + +import nh3 + # # Filter lookup expressions # @@ -29,6 +33,35 @@ FILTER_NEGATION_LOOKUP_MAP = {"n": "exact"} +# +# User input sanitization +# + +# Subset of the HTML tags allowed by default by ammonia: +# https://github.com/rust-ammonia/ammonia/blob/master/src/lib.rs +HTML_ALLOWED_TAGS = nh3.ALLOWED_TAGS - { + # no image maps at present + "area", + "map", + # no document-level markup at present + "article", + "aside", + "footer", + "header", + "nav", + # miscellaneous out-of-scope for now + "data", + "dfn", + "figcaption", + "figure", +} + +# Variant of the HTML attributes allowed by default by ammonia: +# https://github.com/rust-ammonia/ammonia/blob/master/src/lib.rs +# at present we just copy nh3.ALLOWED_ATTRIBUTES but we can modify this later as desired and appropriate +HTML_ALLOWED_ATTRIBUTES = deepcopy(nh3.ALLOWED_ATTRIBUTES) + + # # HTTP Request META safe copy #
nautobot/utilities/forms/fields.py+12 −6 modified@@ -12,7 +12,9 @@ from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist, ValidationError from django.db.models import Q from django.forms.fields import BoundField, JSONField as _JSONField, InvalidJSONInput +from django.templatetags.static import static from django.urls import reverse +from django.utils.html import format_html from nautobot.extras.utils import FeatureQuery from nautobot.utilities.choices import unpack_grouped_choices @@ -391,12 +393,16 @@ class CommentField(forms.CharField): widget = forms.Textarea default_label = "" - # TODO: Port Markdown cheat sheet to internal documentation - default_helptext = ( - '<i class="mdi mdi-information-outline"></i> ' - '<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">' - "Markdown</a> syntax is supported" - ) + + @property + def default_helptext(self): + # TODO: Port Markdown cheat sheet to internal documentation + return format_html( + '<i class="mdi mdi-information-outline"></i> ' + '<a href="https://www.markdownguide.org/cheat-sheet/#basic-syntax" rel="noopener noreferrer">Markdown</a> ' + 'syntax is supported, as well as <a href="{}#render_markdown">a limited subset of HTML</a>.', + static("docs/additional-features/template-filters.html"), + ) def __init__(self, *args, **kwargs): required = kwargs.pop("required", False)
nautobot/utilities/logging.py+13 −0 modified@@ -4,6 +4,9 @@ import re from django.conf import settings +import nh3 + +from nautobot.utilities import constants logger = logging.getLogger(__name__) @@ -27,3 +30,13 @@ def sanitize(string, replacement="(redacted)"): logger.error('Error in string sanitization using "%s"', sanitizer) return string + + +def clean_html(html): + """Use nh3/ammonia to strip out all HTML tags and attributes except those explicitly permitted.""" + return nh3.clean( + html, + tags=constants.HTML_ALLOWED_TAGS, + attributes=constants.HTML_ALLOWED_ATTRIBUTES, + url_schemes=set(settings.ALLOWED_URL_SCHEMES), + )
nautobot/utilities/templatetags/helpers.py+6 −10 modified@@ -8,13 +8,14 @@ from django.contrib.staticfiles.finders import find from django.templatetags.static import static, StaticNode from django.urls import NoReverseMatch, reverse -from django.utils.html import format_html, strip_tags +from django.utils.html import format_html from django.utils.safestring import mark_safe from markdown import markdown from django_jinja import library from nautobot.utilities.config import get_settings_or_config from nautobot.utilities.forms import TableConfigForm +from nautobot.utilities.logging import clean_html from nautobot.utilities.utils import foreground_color, get_route_for_model, UtilizationData HTML_TRUE = mark_safe('<span class="text-success"><i class="mdi mdi-check-bold" title="Yes"></i></span>') # noqa: S308 @@ -162,18 +163,13 @@ def render_markdown(value): Example: {{ text | render_markdown }} """ - # Strip HTML tags - value = strip_tags(value) - - # Sanitize Markdown links - schemes = "|".join(settings.ALLOWED_URL_SCHEMES) - pattern = rf"\[(.+)\]\((?!({schemes})).*:(.+)\)" - value = re.sub(pattern, "[\\1](\\3)", value, flags=re.IGNORECASE) - # Render Markdown html = markdown(value, extensions=["fenced_code", "tables"]) - return mark_safe(html) # noqa: S308 + # Sanitize rendered HTML + html = clean_html(html) + + return mark_safe(html) # noqa: S308 # suspicious-mark-safe-usage, OK here since we sanitized the string earlier @library.filter()
nautobot/utilities/tests/test_templatetags_helpers.py+19 −4 modified@@ -93,9 +93,24 @@ def test_render_markdown(self): self.assertEqual(render_markdown("*italics*"), "<p><em>italics</em></p>") self.assertEqual(render_markdown("**bold and _italics_**"), "<p><strong>bold and <em>italics</em></strong></p>") self.assertEqual(render_markdown("* list"), "<ul>\n<li>list</li>\n</ul>") - self.assertEqual( + self.assertHTMLEqual( render_markdown("[I am a link](https://www.example.com)"), - '<p><a href="https://www.example.com">I am a link</a></p>', + '<p><a href="https://www.example.com" rel="noopener noreferrer">I am a link</a></p>', + ) + + def test_render_markdown_security(self): + self.assertEqual(render_markdown('<script>alert("XSS")</script>'), "") + self.assertHTMLEqual( + render_markdown('[link](javascript:alert("XSS"))'), + '<p><a title="XSS" rel="noopener noreferrer">link</a>)</p>', # the trailing ) seems weird to me, but... + ) + self.assertHTMLEqual( + render_markdown( + "[link\nJS]" + "(javascript:" # '(javascript:' + "alert('XSS'))" # 'alert("XSS"))' + ), + '<p><a rel="noopener noreferrer">link JS</a></p>', ) def test_render_json(self): @@ -220,8 +235,8 @@ def test_support_message(self): self.assertHTMLEqual( support_message(), "<p>If further assistance is required, please join the <code>#nautobot</code> channel " - 'on <a href="https://slack.networktocode.com/">Network to Code\'s Slack community</a> ' - "and post your question.</p>", + 'on <a href="https://slack.networktocode.com/" rel="noopener noreferrer">Network to Code\'s ' + "Slack community</a> and post your question.</p>", ) with override_config(SUPPORT_MESSAGE="Reach out to your support team for assistance."):
poetry.lock+32 −6 modified@@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "amqp" @@ -1780,6 +1780,7 @@ description = "Powerful and Pythonic XML processing library combining libxml2/li optional = true python-versions = ">=3.6" files = [ + {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"}, {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"}, {file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"}, {file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"}, @@ -1789,6 +1790,7 @@ files = [ {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"}, {file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"}, {file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"}, {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"}, {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"}, {file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"}, @@ -1798,6 +1800,7 @@ files = [ {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"}, {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"}, {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"}, {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"}, {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"}, {file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"}, @@ -1823,15 +1826,16 @@ files = [ {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"}, {file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"}, {file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"}, + {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"}, {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"}, - {file = "lxml-5.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cfbac9f6149174f76df7e08c2e28b19d74aed90cad60383ad8671d3af7d0502f"}, {file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"}, {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"}, {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"}, {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1"}, {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"}, {file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"}, {file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"}, {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"}, {file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"}, {file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"}, @@ -2272,6 +2276,31 @@ files = [ [package.extras] optionals = ["jsonschema (>=4.17.3,<5.0.0)", "napalm (>=4.0.0,<5.0.0)"] +[[package]] +name = "nh3" +version = "0.2.15" +description = "Python bindings to the ammonia HTML sanitization library." +optional = false +python-versions = "*" +files = [ + {file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0"}, + {file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5"}, + {file = "nh3-0.2.15-cp37-abi3-win32.whl", hash = "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601"}, + {file = "nh3-0.2.15-cp37-abi3-win_amd64.whl", hash = "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e"}, + {file = "nh3-0.2.15.tar.gz", hash = "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3"}, +] + [[package]] name = "ntc-templates" version = "4.1.0" @@ -2529,7 +2558,6 @@ files = [ {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, @@ -2538,8 +2566,6 @@ files = [ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, @@ -3994,4 +4020,4 @@ sso = ["social-auth-core"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.12" -content-hash = "8877c8b218c8101a76025265d0e7eee41c60a8365a6baa8ea93e311190179d72" +content-hash = "0fa3435f9f81f688448e42195d13706a98a3cf1e4c0b1a15bfdb8db1d633c591"
pyproject.toml+2 −0 modified@@ -119,6 +119,8 @@ netaddr = "~0.8.0" # Note: netutils is limited in scope, dependencies, and observes semver, as such # we permit a looser (^) version constraint here. netutils = "^1.5.0" +# HTML sanitization +nh3 = "~0.2.15" # Handling of version numbers packaging = ">=23.0,<23.2" # Image processing library
17effcbe84a7Sanitize `render_markdown()` output with `nh3` library (#5133)
18 files changed · +221 −46
changes/5133.added+1 −0 added@@ -0,0 +1 @@ +Enhanced Markdown-supporting fields (`comments`, `description`, Notes, Job log entries, etc.) to also permit the use of a limited subset of "safe" HTML tags and attributes.
changes/5133.dependencies+1 −0 added@@ -0,0 +1 @@ +Added `nh3` HTML sanitization library as a dependency.
changes/5133.security+1 −0 added@@ -0,0 +1 @@ +Fixed an XSS vulnerability ([GHSA-v4xv-795h-rv4h](https://github.com/nautobot/nautobot/security/advisories/GHSA-v4xv-795h-rv4h)) in the `render_markdown()` utility function used to render comments, notes, job log entries, etc.
nautobot/core/constants.py+33 −0 modified@@ -1,3 +1,7 @@ +from copy import deepcopy + +import nh3 + SEARCH_MAX_RESULTS = 15 # @@ -32,6 +36,35 @@ FILTER_NEGATION_LOOKUP_MAP = {"n": "exact"} +# +# User input sanitization +# + +# Subset of the HTML tags allowed by default by ammonia: +# https://github.com/rust-ammonia/ammonia/blob/master/src/lib.rs +HTML_ALLOWED_TAGS = nh3.ALLOWED_TAGS - { + # no image maps at present + "area", + "map", + # no document-level markup at present + "article", + "aside", + "footer", + "header", + "nav", + # miscellaneous out-of-scope for now + "data", + "dfn", + "figcaption", + "figure", +} + +# Variant of the HTML attributes allowed by default by ammonia: +# https://github.com/rust-ammonia/ammonia/blob/master/src/lib.rs +# at present we just copy nh3.ALLOWED_ATTRIBUTES but we can modify this later as desired and appropriate +HTML_ALLOWED_ATTRIBUTES = deepcopy(nh3.ALLOWED_ATTRIBUTES) + + # # Reserved Names #
nautobot/core/forms/fields.py+12 −6 modified@@ -9,7 +9,9 @@ from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist, ValidationError from django.db.models import Q from django.forms.fields import BoundField, InvalidJSONInput, JSONField as _JSONField +from django.templatetags.static import static from django.urls import reverse +from django.utils.html import format_html import django_filters from netaddr import EUI from netaddr.core import AddrFormatError @@ -372,12 +374,16 @@ class CommentField(django_forms.CharField): widget = django_forms.Textarea default_label = "" - # TODO: Port Markdown cheat sheet to internal documentation - default_helptext = ( - '<i class="mdi mdi-information-outline"></i> ' - '<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">' - "Markdown</a> syntax is supported" - ) + + @property + def default_helptext(self): + # TODO: Port Markdown cheat sheet to internal documentation + return format_html( + '<i class="mdi mdi-information-outline"></i> ' + '<a href="https://www.markdownguide.org/cheat-sheet/#basic-syntax" rel="noopener noreferrer">Markdown</a> ' + 'syntax is supported, as well as <a href="{}#render_markdown">a limited subset of HTML</a>.', + static("docs/user-guide/platform-functionality/template-filters.html"), + ) def __init__(self, *args, **kwargs): required = kwargs.pop("required", False)
nautobot/core/settings.py+2 −1 modified@@ -682,7 +682,8 @@ ), "SUPPORT_MESSAGE": ConstanceConfigItem( default="", - help_text="Help message to include on 4xx and 5xx error pages. Markdown is supported.\n" + help_text="Help message to include on 4xx and 5xx error pages. " + "Markdown is supported, as are some HTML tags and attributes.\n" "If unspecified, instructions to join Network to Code's Slack community will be provided.", ), }
nautobot/core/templatetags/helpers.py+5 −10 modified@@ -8,7 +8,7 @@ from django.contrib.staticfiles.finders import find from django.templatetags.static import static, StaticNode from django.urls import NoReverseMatch, reverse -from django.utils.html import format_html, strip_tags +from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.text import slugify as django_slugify from django_jinja import library @@ -17,7 +17,7 @@ from nautobot.apps.config import get_app_settings_or_config from nautobot.core import forms -from nautobot.core.utils import color, config, data, lookup +from nautobot.core.utils import color, config, data, logging as nautobot_logging, lookup from nautobot.core.utils.requests import add_nautobot_version_query_param_to_url # S308 is suspicious-mark-safe-usage, but these are all using static strings that we know to be safe @@ -170,17 +170,12 @@ def render_markdown(value): Example: {{ text | render_markdown }} """ - # Strip HTML tags - value = strip_tags(value) - - # Sanitize Markdown links - schemes = "|".join(settings.ALLOWED_URL_SCHEMES) - pattern = rf"\[(.+)\]\((?!({schemes})).*:(.+)\)" - value = re.sub(pattern, "[\\1](\\3)", value, flags=re.IGNORECASE) - # Render Markdown html = markdown(value, extensions=["fenced_code", "tables"]) + # Sanitize rendered HTML + html = nautobot_logging.clean_html(html) + return mark_safe(html) # noqa: S308 # suspicious-mark-safe-usage, OK here since we sanitized the string earlier
nautobot/core/tests/test_templatetags_helpers.py+19 −4 modified@@ -79,9 +79,24 @@ def test_render_markdown(self): helpers.render_markdown("**bold and _italics_**"), "<p><strong>bold and <em>italics</em></strong></p>" ) self.assertEqual(helpers.render_markdown("* list"), "<ul>\n<li>list</li>\n</ul>") - self.assertEqual( + self.assertHTMLEqual( helpers.render_markdown("[I am a link](https://www.example.com)"), - '<p><a href="https://www.example.com">I am a link</a></p>', + '<p><a href="https://www.example.com" rel="noopener noreferrer">I am a link</a></p>', + ) + + def test_render_markdown_security(self): + self.assertEqual(helpers.render_markdown('<script>alert("XSS")</script>'), "") + self.assertHTMLEqual( + helpers.render_markdown('[link](javascript:alert("XSS"))'), + '<p><a title="XSS" rel="noopener noreferrer">link</a>)</p>', # the trailing ) seems weird to me, but... + ) + self.assertHTMLEqual( + helpers.render_markdown( + "[link\nJS]" + "(javascript:" # '(javascript:' + "alert('XSS'))" # 'alert("XSS"))' + ), + '<p><a rel="noopener noreferrer">link JS</a></p>', ) def test_render_json(self): @@ -240,8 +255,8 @@ def test_support_message(self): self.assertHTMLEqual( helpers.support_message(), "<p>If further assistance is required, please join the <code>#nautobot</code> channel " - 'on <a href="https://slack.networktocode.com/">Network to Code\'s Slack community</a> ' - "and post your question.</p>", + 'on <a href="https://slack.networktocode.com/" rel="noopener noreferrer">Network to Code\'s ' + "Slack community</a> and post your question.</p>", ) with override_config(SUPPORT_MESSAGE="Reach out to your support team for assistance."):
nautobot/core/tests/test_views.py+7 −7 modified@@ -366,9 +366,9 @@ def test_404_default_support_message(self): self.assertContains(response, "Network to Code", status_code=404) response_content = response.content.decode(response.charset) self.assertInHTML( - "If further assistance is required, please join the <code>#nautobot</code> channel " - 'on <a href="https://slack.networktocode.com/">Network to Code\'s Slack community</a> ' - "and post your question.", + "If further assistance is required, please join the <code>#nautobot</code> channel on " + '<a href="https://slack.networktocode.com/" rel="noopener noreferrer">Network to Code\'s ' + "Slack community</a> and post your question.", response_content, ) @@ -392,16 +392,16 @@ def test_500_default_support_message(self, mock_get): self.assertContains(response, "Network to Code", status_code=500) response_content = response.content.decode(response.charset) self.assertInHTML( - "If further assistance is required, please join the <code>#nautobot</code> channel " - 'on <a href="https://slack.networktocode.com/">Network to Code\'s Slack community</a> ' - "and post your question.", + "If further assistance is required, please join the <code>#nautobot</code> channel on " + '<a href="https://slack.networktocode.com/" rel="noopener noreferrer">Network to Code\'s ' + "Slack community</a> and post your question.", response_content, ) @override_settings(DEBUG=False, SUPPORT_MESSAGE="Hello world!") @mock.patch("nautobot.core.views.HomeView.get", side_effect=Exception) def test_500_custom_support_message(self, mock_get): - """Nautobot's custom 500 page should be used and should include a default support message.""" + """Nautobot's custom 500 page should be used and should include a custom support message if defined.""" url = reverse("home") with self.assertTemplateUsed("500.html"): self.client.raise_request_exception = False
nautobot/core/utils/logging.py+14 −1 modified@@ -1,9 +1,12 @@ -"""Utilities for working with log messages and similar features.""" +"""Utilities for working with log messages and similar user-authored data.""" import logging import re from django.conf import settings +import nh3 + +from nautobot.core import constants logger = logging.getLogger(__name__) @@ -47,3 +50,13 @@ def sanitize(dirty, replacement="(redacted)"): logger.warning("No sanitizer support for %s data", type(dirty)) return dirty + + +def clean_html(html): + """Use nh3/ammonia to strip out all HTML tags and attributes except those explicitly permitted.""" + return nh3.clean( + html, + tags=constants.HTML_ALLOWED_TAGS, + attributes=constants.HTML_ALLOWED_ATTRIBUTES, + url_schemes=set(settings.ALLOWED_URL_SCHEMES), + )
nautobot/docs/development/jobs/index.md+2 −2 modified@@ -113,7 +113,7 @@ This is the human-friendly name of your job, as will be displayed in the Nautobo #### `description` An optional human-friendly description of what this job does. -This can accept either plain text or Markdown-formatted text. It can also be multiple lines: +This can accept either plain text, Markdown-formatted text, or [a limited subset of HTML](../../user-guide/platform-functionality/template-filters.md#render_markdown). It can also be multiple lines: ```python class ExampleJob(Job): @@ -512,7 +512,7 @@ To skip writing a log entry to the database, set the `skip_db_logging` key in th logger.info("This job is running!", extra={"skip_db_logging": True}) ``` -Markdown rendering is supported for log messages. +Markdown rendering is supported for log messages, as well as [a limited subset of HTML](../../user-guide/platform-functionality/template-filters.md#render_markdown). +/- 1.3.4 As a security measure, the `message` passed to any of these methods will be passed through the `nautobot.core.utils.logging.sanitize()` function in an attempt to strip out information such as usernames/passwords that should not be saved to the logs. This is of course best-effort only, and Job authors should take pains to ensure that such information is not passed to the logging APIs in the first place. The set of redaction rules used by the `sanitize()` function can be configured as [settings.SANITIZER_PATTERNS](../../user-guide/administration/configuration/optional-settings.md#sanitizer_patterns).
nautobot/docs/user-guide/administration/configuration/optional-settings.md+1 −1 modified@@ -964,7 +964,7 @@ If set to `False`, unknown/unrecognized filter parameters will be discarded and Default: `""` -A message to include on error pages (status code 403, 404, 500, etc.) when an error occurs. You can configure this to direct users to the appropriate contact(s) within your organization that provide support for Nautobot. Markdown formatting is supported within this message (raw HTML is not). +A message to include on error pages (status code 403, 404, 500, etc.) when an error occurs. You can configure this to direct users to the appropriate contact(s) within your organization that provide support for Nautobot. Markdown formatting is supported within this message, as well as [a limited subset of HTML](../../platform-functionality/template-filters.md#render_markdown). If unset, the default message that will appear is `If further assistance is required, please join the #nautobot channel on [Network to Code's Slack community](https://slack.networktocode.com) and post your question.`
nautobot/docs/user-guide/platform-functionality/note.md+1 −1 modified@@ -4,4 +4,4 @@ Notes provide a place for you to store notes or general information on an object, such as a Device, that may not require a specific field for. This could be a note on a recent upgrade, a warning about a problematic device, or the reason the Rack was marked with the Status `Retired`. -The note field supports [Markdown Basic Syntax](https://www.markdownguide.org/cheat-sheet/#basic-syntax). +The note field supports [Markdown Basic Syntax](https://www.markdownguide.org/cheat-sheet/#basic-syntax) as well as [a limited subset of HTML](template-filters.md#render_markdown).
nautobot/docs/user-guide/platform-functionality/template-filters.md+75 −1 modified@@ -216,12 +216,86 @@ Render a dictionary as formatted JSON. ### render_markdown -Render text as Markdown. +Render and sanitize Markdown text into HTML. A limited subset of HTML tags and attributes are permitted in the text as well; non-permitted HTML will be stripped from the output for security. ```django {{ text | render_markdown }} ``` +#### Permitted HTML Tags and Attributes + ++++ 2.1.2 + +The set of permitted HTML tags is defined in `nautobot.core.constants.HTML_ALLOWED_TAGS`, and their permitted attributes are defined in `nautobot.core.constants.HTML_ALLOWED_ATTRIBUTES`. As of Nautobot 2.1.2 the following are permitted: + +??? info "Full list of HTML tags and attributes" + | Tag | Attributes | + | -------------- | -------------------------------------------------------------------- | + | `<a>` | `href`, `hreflang` | + | `<abbr>` | | + | `<acronym>` | | + | `<b>` | | + | `<bdi>` | | + | `<bdo>` | `dir` | + | `<blockquote>` | `cite` | + | `<br>` | | + | `<caption>` | | + | `<center>` | | + | `<cite>` | | + | `<code>` | | + | `<col>` | `align`, `char`, `charoff`, `span` | + | `<colgroup>` | `align`, `char`, `charoff`, `span` | + | `<dd>` | | + | `<del>` | `cite`, `datetime` | + | `<details>` | | + | `<div>` | | + | `<dl>` | | + | `<dt>` | | + | `<em>` | | + | `<h1>` | | + | `<h2>` | | + | `<h3>` | | + | `<h4>` | | + | `<h5>` | | + | `<h6>` | | + | `<hgroup>` | | + | `<hr>` | `align`, `size`, `width` | + | `<i>` | | + | `<img>` | `align`, `alt`, `height`, `src`, `width` | + | `<ins>` | `cite`, `datetime` | + | `<kbd>` | | + | `<li>` | | + | `<mark>` | | + | `<ol>` | `start` | + | `<p>` | | + | `<pre>` | | + | `<q>` | `cite` | + | `<rp>` | | + | `<rt>` | | + | `<rtc>` | | + | `<ruby>` | | + | `<s>` | | + | `<samp>` | | + | `<small>` | | + | `<span>` | | + | `<strike>` | | + | `<strong>` | | + | `<sub>` | | + | `<summary>` | | + | `<sup>` | | + | `<table>` | `align`, `char`, `charoff`, `summary` | + | `<tbody>` | `align`, `char`, `charoff` | + | `<td>` | `align`, `char`, `charoff`, `colspan`, `headers`, `rowspan` | + | `<th>` | `align`, `char`, `charoff`, `colspan`, `headers`, `rowspan`, `scope` | + | `<thead>` | `align`, `char`, `charoff` | + | `<time>` | | + | `<tr>` | `align`, `char`, `charoff` | + | `<tt>` | | + | `<u>` | | + | `<ul>` | | + | `<var>` | | + | `<wbr>` | | + ### render_yaml Render a dictionary as formatted YAML.
nautobot/extras/forms/forms.py+9 −5 modified@@ -368,6 +368,12 @@ class ConfigContextSchemaFilterForm(BootstrapMixin, forms.Form): ) +class CustomFieldDescriptionField(CommentField): + @property + def default_helptext(self): + return "Also used as the help text when editing models using this custom field.<br>" + super().default_helptext + + class CustomFieldForm(BootstrapMixin, forms.ModelForm): label = forms.CharField(required=True, max_length=50, help_text="Name of the field as displayed to users.") key = SlugField( @@ -376,11 +382,9 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): slug_source="label", help_text="Internal name of this field. Please use underscores rather than dashes.", ) - description = forms.CharField( + description = CustomFieldDescriptionField( + label="Description", required=False, - help_text="Also used as the help text when editing models using this custom field.<br>" - '<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">' - "Markdown</a> syntax is supported.", ) content_types = MultipleContentTypeField( feature="custom_fields", help_text="The object(s) to which this field applies." @@ -1098,7 +1102,7 @@ class JobButtonFilterForm(BootstrapMixin, forms.Form): class NoteForm(BootstrapMixin, forms.ModelForm): - note = CommentField + note = CommentField() class Meta: model = Note
nautobot/extras/models/jobs.py+4 −1 modified@@ -108,7 +108,10 @@ class Job(PrimaryModel): help_text="Human-readable name of this job", unique=True, ) - description = models.TextField(blank=True, help_text="Markdown formatting is supported") + description = models.TextField( + blank=True, + help_text="Markdown formatting and a limited subset of HTML are supported", + ) # Control flags installed = models.BooleanField(
poetry.lock+32 −6 modified@@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "amqp" @@ -1642,6 +1642,7 @@ description = "Powerful and Pythonic XML processing library combining libxml2/li optional = true python-versions = ">=3.6" files = [ + {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"}, {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"}, {file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"}, {file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"}, @@ -1651,6 +1652,7 @@ files = [ {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"}, {file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"}, {file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"}, {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"}, {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"}, {file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"}, @@ -1660,6 +1662,7 @@ files = [ {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"}, {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"}, {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"}, {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"}, {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"}, {file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"}, @@ -1685,15 +1688,16 @@ files = [ {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"}, {file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"}, {file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"}, + {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"}, {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"}, - {file = "lxml-5.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cfbac9f6149174f76df7e08c2e28b19d74aed90cad60383ad8671d3af7d0502f"}, {file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"}, {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"}, {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"}, {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1"}, {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"}, {file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"}, {file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"}, {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"}, {file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"}, {file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"}, @@ -2171,6 +2175,31 @@ files = [ [package.extras] optionals = ["jsonschema (>=4.17.3,<5.0.0)", "napalm (>=4.0.0,<5.0.0)"] +[[package]] +name = "nh3" +version = "0.2.15" +description = "Python bindings to the ammonia HTML sanitization library." +optional = false +python-versions = "*" +files = [ + {file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0"}, + {file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5"}, + {file = "nh3-0.2.15-cp37-abi3-win32.whl", hash = "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601"}, + {file = "nh3-0.2.15-cp37-abi3-win_amd64.whl", hash = "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e"}, + {file = "nh3-0.2.15.tar.gz", hash = "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3"}, +] + [[package]] name = "ntc-templates" version = "4.1.0" @@ -2428,7 +2457,6 @@ files = [ {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, @@ -2437,8 +2465,6 @@ files = [ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, @@ -3957,4 +3983,4 @@ sso = ["social-auth-core"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.12" -content-hash = "8138e21579db269494e43aa441e2690b0bf838b58c6cce161dcb9f4cd1852640" +content-hash = "136fa4c518d4206074a6f43319274580a9fd1c9ccc7e32b8045e767a0f8bcd12"
pyproject.toml+2 −0 modified@@ -112,6 +112,8 @@ netaddr = "~0.8.0" # Note: netutils is limited in scope, dependencies, and observes semver, as such # we permit a looser (^) version constraint here. netutils = "^1.6.0" +# HTML sanitization +nh3 = "~0.2.15" # Handling of version numbers packaging = ">=23.1" # Image processing library
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-v4xv-795h-rv4hghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-23345ghsaADVISORY
- github.com/nautobot/nautobot/commit/17effcbe84a72150c82b138565c311bbee357e80ghsax_refsource_MISCWEB
- github.com/nautobot/nautobot/commit/64312a4297b5ca49b6cdedf477e41e8e4fd61cceghsax_refsource_MISCWEB
- github.com/nautobot/nautobot/pull/5133ghsax_refsource_MISCWEB
- github.com/nautobot/nautobot/pull/5134ghsax_refsource_MISCWEB
- github.com/nautobot/nautobot/security/advisories/GHSA-v4xv-795h-rv4hghsax_refsource_CONFIRMWEB
- github.com/pypa/advisory-database/tree/main/vulns/nautobot/PYSEC-2024-16.yamlghsaWEB
News mentions
0No linked articles in our index yet.