Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs/content/en/open_source/upgrading/2.57.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
title: 'Upgrading to DefectDojo Version 2.57.x'
toc_hide: true
weight: -20260302
description: No special instructions.
description: HTML sanitization library replaced (bleach → nh3).
---
There are no special instructions for upgrading to 2.57.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.57.0) for the contents of the release.
Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.57.0) for the contents of the release.

## HTML sanitization: bleach replaced by nh3

The `bleach` library has been replaced by [`nh3`](https://nh3.readthedocs.io/) for HTML sanitization. This is a drop-in replacement in most cases, but there are two minor behavioral changes to be aware of:

- **`style` attributes are no longer allowed.** `bleach` supported CSS property-level filtering (e.g. allowing only `color` or `font-weight`). `nh3` has no equivalent, so `style` attributes are stripped entirely to avoid allowing arbitrary CSS injection. Content that previously relied on inline styles (e.g. colored text in the login banner, background-color on markdown images) will lose that styling.
- **Disallowed tags are stripped rather than escaped.** Previously, a tag like `<script>` would be rendered as the literal text `&lt;script&gt;`. Now the tag is removed entirely and only its text content is kept. This is the correct behavior for a sanitizer.
37 changes: 16 additions & 21 deletions dojo/templatetags/display_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
from itertools import chain
from pathlib import Path

import bleach
import dateutil.relativedelta
import markdown
from bleach.css_sanitizer import CSSSanitizer
import nh3
from django import template
from django.conf import settings
from django.contrib.auth.models import User
Expand Down Expand Up @@ -50,17 +49,21 @@
}

markdown_attrs = {
"*": ["id"],
"img": ["src", "alt", "title", "width", "height", "style"],
"a": ["href", "alt", "target", "title"],
"span": ["class"], # used for code highlighting
"pre": ["class"], # used for code highlighting
"div": ["class"], # used for code highlighting
"*": {"id"},
"img": {"src", "alt", "title", "width", "height"},
"a": {"href", "alt", "target", "title"},
"span": {"class"}, # used for code highlighting
"pre": {"class"}, # used for code highlighting
"div": {"class"}, # used for code highlighting
}

markdown_styles = [
"background-color",
]
# nh3 base allowlist (equivalent to bleach.ALLOWED_TAGS / bleach.ALLOWED_ATTRIBUTES)
_NH3_ALLOWED_TAGS = {"a", "abbr", "acronym", "b", "blockquote", "code", "em", "i", "li", "ol", "strong", "ul"}
_NH3_ALLOWED_ATTRIBUTES = {
"a": {"href", "title", "target"},
"abbr": {"title"},
"acronym": {"title"},
}

finding_related_action_classes_dict = {
"reset_finding_duplicate_status": "fa-solid fa-eraser",
Expand Down Expand Up @@ -91,21 +94,13 @@ def markdown_render(value):
"markdown.extensions.fenced_code",
"markdown.extensions.toc",
"markdown.extensions.tables"])
return mark_safe(bleach.clean(markdown_text, tags=markdown_tags, attributes=markdown_attrs, css_sanitizer=markdown_styles))
return mark_safe(nh3.clean(markdown_text, tags=markdown_tags, attributes=markdown_attrs))
return None


@register.filter
def bleach_with_a_tags(message):
# Create a copy of ALLOWED_ATTRIBUTES to avoid mutating the global
allowed_attributes = {
**bleach.ALLOWED_ATTRIBUTES,
"a": [*bleach.ALLOWED_ATTRIBUTES.get("a", []), "style", "target"],
}
return mark_safe(bleach.clean(
message,
attributes=allowed_attributes,
css_sanitizer=CSSSanitizer(allowed_css_properties=["color", "font-weight"])))
return mark_safe(nh3.clean(message, tags=_NH3_ALLOWED_TAGS, attributes=_NH3_ALLOWED_ATTRIBUTES))


def text_shortener(value, length):
Expand Down
16 changes: 4 additions & 12 deletions dojo/templatetags/get_banner.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import bleach
from bleach.css_sanitizer import CSSSanitizer
import nh3
from django import template
from django.utils.safestring import mark_safe

from dojo.models import BannerConf
from dojo.templatetags.display_tags import _NH3_ALLOWED_ATTRIBUTES, _NH3_ALLOWED_TAGS

register = template.Library()

Expand All @@ -15,16 +15,8 @@ def get_banner_conf(attribute):
value = getattr(banner_config, attribute, None)
if value:
if attribute == "banner_message":
# only admin can edit login banner, so we allow html, but still bleach it
# Create a copy of ALLOWED_ATTRIBUTES to avoid mutating the global
allowed_attributes = {
**bleach.ALLOWED_ATTRIBUTES,
"a": [*bleach.ALLOWED_ATTRIBUTES.get("a", []), "style", "target"],
}
return mark_safe(bleach.clean(
value,
attributes=allowed_attributes,
css_sanitizer=CSSSanitizer(allowed_css_properties=["color", "font-weight"])))
# only admin can edit login banner, so we allow html, but still sanitize it
return mark_safe(nh3.clean(value, tags=_NH3_ALLOWED_TAGS, attributes=_NH3_ALLOWED_ATTRIBUTES))
return value
except Exception:
return False
Expand Down
12 changes: 3 additions & 9 deletions dojo/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from math import pi, sqrt
from pathlib import Path

import bleach
import crum
import cvss
import vobject
Expand All @@ -41,6 +40,7 @@
from django.shortcuts import redirect as django_redirect
from django.urls import get_resolver, get_script_prefix, reverse
from django.utils import timezone
from django.utils.html import escape
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.translation import gettext as _
from kombu import Connection
Expand Down Expand Up @@ -1812,14 +1812,8 @@ def get_current_request():


def create_bleached_link(url, title):
link = '<a href="'
link += url
link += '" target="_blank" title="'
link += title
link += '">'
link += title
link += "</a>"
return bleach.clean(link, tags={"a"}, attributes={"a": ["href", "target", "title"]})
# escape() encodes text into HTML — the right tool for embedding URLs into attributes, not a sanitizer like nh3
return f'<a href="{escape(url)}" target="_blank" title="{escape(title)}" rel="noopener noreferrer">{escape(title)}</a>'


def get_object_or_none(klass, *args, **kwargs):
Expand Down
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# requirements.txt for DefectDojo using Python 3.x
bleach==6.3.0
bleach[css]
nh3
celery[sqs]==5.6.2
# pycurl and boto3 are included via celery[sqs] for Celery Broker AWS (SQS) support
defusedxml==0.7.1
Expand Down
4 changes: 2 additions & 2 deletions tests/announcement_banner_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,11 @@ def test_html_announcement(self):
driver.get(self.base_url)
self.assertFalse(self.is_element_by_css_selector_present(".announcement-banner"))

text = "Links in announcements? <a href='https://github.com/DefectDojo/django-DefectDojo' style='color: #224477;' target='_blank'>you bet!</a>"
text = "Links in announcements? <a href='https://github.com/DefectDojo/django-DefectDojo' target='_blank'>you bet!</a>"
self.enable_announcement(text, dismissable=False, style=self.type)
self.assertTrue(self.is_success_message_present("Announcement updated successfully."))

driver.find_element(By.XPATH, "//div[contains(@class, 'announcement-banner')]/a[@href='https://github.com/DefectDojo/django-DefectDojo' and @style='color: #224477;' and @target='_blank']")
driver.find_element(By.XPATH, "//div[contains(@class, 'announcement-banner')]/a[@href='https://github.com/DefectDojo/django-DefectDojo' and @target='_blank']")
self.disable_announcement()
self.assertTrue(self.is_success_message_present("Announcement removed for everyone."))

Expand Down
Loading