From 90703c45534b6cba170ecdcfae963c969464efbd Mon Sep 17 00:00:00 2001
From: Wes Appler <145372368+wes-otf@users.noreply.github.com>
Date: Mon, 11 Mar 2024 08:11:12 -0400
Subject: [PATCH] Replace bleach with nh3 (#3696)

Fixes #3693

Aims to fully replace bleach with nh3 due to bleach deprecation.
Currently, [django-nh3](https://github.com/marksweb/django-nh3) is in
it's infancy, but seems like it could be an almost drop in replacement
for [django-bleach](https://github.com/marksweb/django-bleach), for I
[forked it](https://github.com/wes-otf/django-nh3) and made some small
additions that would allow it to work for our purposes and be smoothly
migrated.

Initial smoke testing in Hypha seems to work exactly as bleach did but
needs more extensive testing. Ideally I would smooth out some edges of
my fork and put in a PR to django-nh3. Let me know any
thoughts/questions!
---
 .../activity/include/listing_base.html        |  6 +-
 .../include/notifications_dropdown.html       |  2 +-
 .../templates/activity/notifications.html     |  3 +-
 .../messages/email/determination.html         |  4 +-
 hypha/apply/api/v1/serializers.py             |  4 +-
 .../dashboard/applicant_dashboard.html        |  4 +-
 .../dashboard/contracting_dashboard.html      |  4 +-
 .../templates/dashboard/dashboard.html        |  4 +-
 .../dashboard/finance_dashboard.html          |  4 +-
 hypha/apply/determinations/models.py          |  4 +-
 .../base_determination_form.html              |  2 +-
 .../determinations/determination_detail.html  |  8 +--
 hypha/apply/funds/differ.py                   | 26 +++++++--
 hypha/apply/funds/forms.py                    |  4 +-
 .../application_projects/report_detail.html   |  6 +-
 .../application_projects/vendor_detail.html   |  6 +-
 .../review/render_scored_answer_field.html    |  4 +-
 .../templates/review/review_detail.html       |  2 +-
 .../review/templates/review/review_list.html  |  4 +-
 hypha/apply/stream_forms/blocks.py            | 10 ++--
 .../stream_forms/render_markdown_field.html   |  4 +-
 .../stream_forms/render_unsafe_field.html     |  8 +--
 .../apply/templates/forms/includes/field.html |  4 +-
 hypha/apply/utils/blocks.py                   |  6 +-
 .../apply_home/includes/apply_listing.html    |  4 +-
 .../news/templates/news/news_index.html       | 56 +++++++++++++++++++
 hypha/settings/base.py                        | 30 +++++-----
 hypha/settings/django.py                      |  2 +-
 requirements.txt                              |  2 +-
 29 files changed, 149 insertions(+), 78 deletions(-)
 create mode 100644 hypha/public/news/templates/news/news_index.html

diff --git a/hypha/apply/activity/templates/activity/include/listing_base.html b/hypha/apply/activity/templates/activity/include/listing_base.html
index 2336b81eb..af43a912d 100644
--- a/hypha/apply/activity/templates/activity/include/listing_base.html
+++ b/hypha/apply/activity/templates/activity/include/listing_base.html
@@ -1,4 +1,4 @@
-{% load i18n activity_tags bleach_tags markdown_tags submission_tags apply_tags heroicons %}
+{% load i18n activity_tags nh3_tags markdown_tags submission_tags apply_tags heroicons %}
 
 <div class="feed__item feed__item--{{ activity.type }} border shadow-sm rounded-sm pb-2 " id="communications#{{ activity.id }}">
     <div class="feed__pre-content hidden lg:block">
@@ -48,7 +48,7 @@
                      data-visibility="{{activity.visibility}}"
                      data-edit-url="{% url 'api:v1:comments-edit' pk=activity.pk %}"
                 >
-                    {{ activity|display_for:request.user|submission_links|markdown|bleach }}
+                    {{ activity|display_for:request.user|submission_links|markdown|nh3 }}
                 </div>
                 <style>
                     @media only screen and (min-width: 1024px){
@@ -60,7 +60,7 @@
                 <div class="js-edit-block pe-3" aria-live="polite"></div>
             {% else %}
                 <div class="px-3 prose">
-                    {{ activity|display_for:request.user|submission_links|markdown|bleach }}
+                    {{ activity|display_for:request.user|submission_links|markdown|nh3 }}
                 </div>
             {% endif %}
 
diff --git a/hypha/apply/activity/templates/activity/include/notifications_dropdown.html b/hypha/apply/activity/templates/activity/include/notifications_dropdown.html
index 65d7830fb..0866fcae5 100644
--- a/hypha/apply/activity/templates/activity/include/notifications_dropdown.html
+++ b/hypha/apply/activity/templates/activity/include/notifications_dropdown.html
@@ -1,4 +1,4 @@
-{% load i18n activity_tags bleach_tags markdown_tags submission_tags apply_tags %}
+{% load i18n activity_tags nh3_tags markdown_tags submission_tags apply_tags %}
 
 <div class="notifications notifications--dropdown">
     <div class="notifications__content zeta" role="activity">
diff --git a/hypha/apply/activity/templates/activity/notifications.html b/hypha/apply/activity/templates/activity/notifications.html
index 647b09ca3..130b90d03 100644
--- a/hypha/apply/activity/templates/activity/notifications.html
+++ b/hypha/apply/activity/templates/activity/notifications.html
@@ -1,6 +1,5 @@
 {% extends "base-apply.html" %}
-{% load i18n static activity_tags apply_tags bleach_tags markdown_tags submission_tags heroicons %}
-
+{% load i18n static activity_tags apply_tags nh3_tags markdown_tags submission_tags heroicons %}
 {% block content %}
     <div class="admin-bar">
         <div class="admin-bar__inner">
diff --git a/hypha/apply/activity/templates/messages/email/determination.html b/hypha/apply/activity/templates/messages/email/determination.html
index 99f53547d..325d559b1 100644
--- a/hypha/apply/activity/templates/messages/email/determination.html
+++ b/hypha/apply/activity/templates/messages/email/determination.html
@@ -1,8 +1,8 @@
 {% extends "messages/email/applicant_base.html" %}
-{% load bleach_tags i18n %}
+{% load nh3_tags i18n %}
 
 {% block content %}{% trans "Your application has been reviewed and the outcome is" %}: {{ determination.clean_outcome }}
 
-    {{ determination.message|bleach|striptags }}
+    {{ determination.message|nh3|striptags }}
 
     {% trans "Read the full determination here" %}: {{ request.scheme }}://{{ request.get_host }}{{ determination.get_absolute_url }}{% endblock %}
diff --git a/hypha/apply/api/v1/serializers.py b/hypha/apply/api/v1/serializers.py
index 01288cc79..d80715f24 100644
--- a/hypha/apply/api/v1/serializers.py
+++ b/hypha/apply/api/v1/serializers.py
@@ -1,5 +1,5 @@
 from django.contrib.auth import get_user_model
-from django_bleach.templatetags.bleach_tags import bleach_value
+from django_nh3.templatetags.nh3_tags import nh3_value
 from rest_framework import serializers
 
 from hypha.apply.activity.models import Activity
@@ -416,7 +416,7 @@ class CommentSerializer(serializers.ModelSerializer):
         )
 
     def get_message(self, obj):
-        return bleach_value(markdown_to_html(obj.message))
+        return nh3_value(markdown_to_html(obj.message))
 
     def get_editable(self, obj):
         return self.context["request"].user == obj.user
diff --git a/hypha/apply/dashboard/templates/dashboard/applicant_dashboard.html b/hypha/apply/dashboard/templates/dashboard/applicant_dashboard.html
index 6d53d17f8..992c709d4 100644
--- a/hypha/apply/dashboard/templates/dashboard/applicant_dashboard.html
+++ b/hypha/apply/dashboard/templates/dashboard/applicant_dashboard.html
@@ -1,6 +1,6 @@
 {% extends "base-apply.html" %}
 {% load render_table from django_tables2 %}
-{% load i18n static wagtailcore_tags workflow_tags statusbar_tags heroicons dashboard_statusbar_tags apply_tags invoice_tools markdown_tags bleach_tags %}
+{% load i18n static wagtailcore_tags workflow_tags statusbar_tags heroicons dashboard_statusbar_tags apply_tags invoice_tools markdown_tags nh3_tags %}
 {% block body_class %}bg-light-grey{% endblock %}
 
 {% block title %}{% trans "Dashboard" %}{% endblock %}
@@ -31,7 +31,7 @@
                 {% for task in my_tasks.data %}
                     <div class="bg-white p-1 flex mb-1 items-center">
                         <svg class="icon icon--dashboard-tasks"><use xlink:href="#{{ task.icon }}"></use></svg>
-                        <div class="flex-1">{{ task.text|markdown|bleach }}</div>
+                        <div class="flex-1">{{ task.text|markdown|nh3 }}</div>
                         <a class="button button-primary m-2" href="{{ task.url }}">View</a>
                     </div>
                 {% endfor %}
diff --git a/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html b/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html
index 3048ee170..2054d55a8 100644
--- a/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html
+++ b/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html
@@ -1,6 +1,6 @@
 {% extends "base-apply.html" %}
 {% load render_table from django_tables2 %}
-{% load i18n static markdown_tags bleach_tags %}
+{% load i18n static markdown_tags nh3_tags %}
 
 {% block title %}{% trans "Dashboard" %}{% endblock %}
 
@@ -21,7 +21,7 @@
                 {% for task in my_tasks.data %}
                     <div class="bg-white p-1 flex mb-1 items-center">
                         <svg class="icon icon--dashboard-tasks"><use xlink:href="#{{ task.icon }}"></use></svg>
-                        <div class="flex-1">{{ task.text|markdown|bleach }}</div>
+                        <div class="flex-1">{{ task.text|markdown|nh3 }}</div>
                         <a class="button button-primary m-2" href="{{ task.url }}">View</a>
                     </div>
                 {% endfor %}
diff --git a/hypha/apply/dashboard/templates/dashboard/dashboard.html b/hypha/apply/dashboard/templates/dashboard/dashboard.html
index 6252c72f6..416674592 100644
--- a/hypha/apply/dashboard/templates/dashboard/dashboard.html
+++ b/hypha/apply/dashboard/templates/dashboard/dashboard.html
@@ -1,6 +1,6 @@
 {% extends "base-apply.html" %}
 {% load render_table from django_tables2 %}
-{% load i18n static bleach_tags markdown_tags %}
+{% load i18n static nh3_tags markdown_tags %}
 
 {% block extra_css %}
     {{ my_reviewed.filterset.form.media.css }}
@@ -30,7 +30,7 @@
                     {% for task in my_tasks.data %}
                         <div class="bg-white p-1 flex mb-1 items-center">
                             <svg class="icon icon--dashboard-tasks"><use xlink:href="#{{ task.icon }}"></use></svg>
-                            <div class="flex-1">{{ task.text|markdown|bleach }}</div>
+                            <div class="flex-1">{{ task.text|markdown|nh3 }}</div>
                             <a class="button button-primary m-2" href="{{ task.url }}">View</a>
                         </div>
                     {% endfor %}
diff --git a/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html b/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html
index 34f6b6962..6320843ea 100644
--- a/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html
+++ b/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html
@@ -1,6 +1,6 @@
 {% extends "base-apply.html" %}
 {% load render_table from django_tables2 %}
-{% load i18n static markdown_tags bleach_tags %}
+{% load i18n static markdown_tags nh3_tags %}
 
 {% block title %}{% trans "Dashboard" %}{% endblock %}
 
@@ -21,7 +21,7 @@
                 {% for task in my_tasks.data %}
                     <div class="bg-white p-1 flex mb-1 items-center">
                         <svg class="icon icon--dashboard-tasks"><use xlink:href="#{{ task.icon }}"></use></svg>
-                        <div class="flex-1">{{ task.text|markdown|bleach }}</div>
+                        <div class="flex-1">{{ task.text|markdown|nh3 }}</div>
                         <a class="button button-primary m-2" href="{{ task.url }}">View</a>
                     </div>
                 {% endfor %}
diff --git a/hypha/apply/determinations/models.py b/hypha/apply/determinations/models.py
index 0707ba0cf..09bbfb22c 100644
--- a/hypha/apply/determinations/models.py
+++ b/hypha/apply/determinations/models.py
@@ -1,4 +1,4 @@
-import bleach
+import nh3
 from django.conf import settings
 from django.core.serializers.json import DjangoJSONEncoder
 from django.db import models
@@ -130,7 +130,7 @@ class Determination(DeterminationFormFieldsMixin, AccessFormData, models.Model):
 
     @property
     def stripped_message(self):
-        return bleach.clean(self.message, tags=[], strip=True)
+        return nh3.clean(self.message, tags=set())
 
     @property
     def clean_outcome(self):
diff --git a/hypha/apply/determinations/templates/determinations/base_determination_form.html b/hypha/apply/determinations/templates/determinations/base_determination_form.html
index ec982f879..153c84bc6 100644
--- a/hypha/apply/determinations/templates/determinations/base_determination_form.html
+++ b/hypha/apply/determinations/templates/determinations/base_determination_form.html
@@ -1,5 +1,5 @@
 {% extends "base-apply.html" %}
-{% load i18n static bleach_tags %}
+{% load i18n static nh3_tags %}
 {% block title %}{% if object %}{% trans "Edit a Determination" %} {% if object.is_draft %}{% trans "draft" %}{% endif %}{% else %}{% trans "Create a Determination" %}{% endif %}{% endblock %}
 {% block content %}
 
diff --git a/hypha/apply/determinations/templates/determinations/determination_detail.html b/hypha/apply/determinations/templates/determinations/determination_detail.html
index c160ddfe2..0ca28fead 100644
--- a/hypha/apply/determinations/templates/determinations/determination_detail.html
+++ b/hypha/apply/determinations/templates/determinations/determination_detail.html
@@ -1,5 +1,5 @@
 {% extends "base-apply.html" %}
-{% load i18n bleach_tags heroicons %}
+{% load i18n nh3_tags heroicons %}
 
 {% block title %}{% trans "Determination for" %} {{ determination.submission.title }}{% endblock %}
 
@@ -32,14 +32,14 @@
 
     <div class="rich-text rich-text--answers prose">
         <h4>{% trans "Determination message" %}</h4>
-        {{ determination.message|bleach }}
+        {{ determination.message|nh3 }}
         {% for group in determination.detailed_data.values %}
             {% if group.title %}
-                <h4>{{ group.title|bleach }}</h4>
+                <h4>{{ group.title|nh3 }}</h4>
             {% endif %}
             {% for question, answer in group.questions %}
                 <h5>{{ question }}</h5>
-                {% if answer %}{% if answer == True %}{{ answer|yesno:"Agree,Disagree" }}{% else %}{{ answer|bleach }}{% endif %}{% else %}-{% endif %}
+                {% if answer %}{% if answer == True %}{{ answer|yesno:"Agree,Disagree" }}{% else %}{{ answer|nh3 }}{% endif %}{% else %}-{% endif %}
             {% endfor %}
         {% endfor %}
     </div>
diff --git a/hypha/apply/funds/differ.py b/hypha/apply/funds/differ.py
index 66ccf9ffb..0c289eb98 100644
--- a/hypha/apply/funds/differ.py
+++ b/hypha/apply/funds/differ.py
@@ -1,7 +1,8 @@
 import re
 from difflib import SequenceMatcher
+from typing import Tuple
 
-from bleach.sanitizer import Cleaner
+import nh3
 from django.utils.html import format_html
 from django.utils.safestring import mark_safe
 
@@ -16,13 +17,26 @@ def wrap_added(text):
     return format_html('<span class="bg-green-200">{}</span>', mark_safe(text))
 
 
-def compare(answer_a, answer_b, should_bleach=True):
-    if should_bleach:
-        cleaner = Cleaner(tags=["h4"], attributes={}, strip=True)
+def compare(answer_a: str, answer_b: str, should_clean: bool = True) -> Tuple[str, str]:
+    """Compare two strings, populate diff HTML and insert it, and return a tuple of the given strings.
+
+    Args:
+        answer_a:
+            The original string
+        answer_b:
+            The string to compare to the original
+        should_clean:
+            Optional boolean to determine if the string should be sanitized with NH3 (default=True)
+
+    Returns:
+        A tuple of the original strings with diff HTML inserted.
+    """
+
+    if should_clean:
         answer_a = re.sub("(<li[^>]*>)", r"\1â—¦ ", answer_a)
         answer_b = re.sub("(<li[^>]*>)", r"\1â—¦ ", answer_b)
-        answer_a = cleaner.clean(answer_a)
-        answer_b = cleaner.clean(answer_b)
+        answer_a = nh3.clean(answer_a, tags={"h4"}, attributes={})
+        answer_b = nh3.clean(answer_b, tags={"h4"}, attributes={})
 
     diff = SequenceMatcher(None, answer_a, answer_b)
     from_diff = []
diff --git a/hypha/apply/funds/forms.py b/hypha/apply/funds/forms.py
index d1face9a8..3921248b0 100644
--- a/hypha/apply/funds/forms.py
+++ b/hypha/apply/funds/forms.py
@@ -3,7 +3,7 @@ from functools import partial
 from itertools import groupby
 from operator import methodcaller
 
-import bleach
+import nh3
 from django import forms
 from django.db.models import Q
 from django.utils.safestring import mark_safe
@@ -448,7 +448,7 @@ def make_role_reviewer_fields():
     staff_reviewers = User.objects.staff().only("full_name", "pk")
 
     for role in ReviewerRole.objects.all().order_by("order"):
-        role_name = bleach.clean(role.name, strip=True)
+        role_name = nh3.clean(role.name, tags=set())
         field_name = f"role_reviewer_{role.id}"
         field = forms.ModelChoiceField(
             queryset=staff_reviewers,
diff --git a/hypha/apply/projects/templates/application_projects/report_detail.html b/hypha/apply/projects/templates/application_projects/report_detail.html
index a2cf266ef..b0209e7be 100644
--- a/hypha/apply/projects/templates/application_projects/report_detail.html
+++ b/hypha/apply/projects/templates/application_projects/report_detail.html
@@ -1,5 +1,5 @@
 {% extends "base-apply.html" %}
-{% load i18n static bleach_tags heroicons %}
+{% load i18n static nh3_tags heroicons %}
 
 {% block title %}{% trans "Report" %} | {{ object.project.title }}{% endblock %}
 {% block body_class %}{% endblock %}
@@ -28,12 +28,12 @@
                 {% else %}
                     <h4>{% trans "Public Report" %}</h4>
                     <div class="rich-text">
-                        {{ object.current.public_content|bleach|safe }}
+                        {{ object.current.public_content|nh3|safe }}
                     </div>
 
                     <h4>{% trans "Private Report" %}</h4>
                     <div class="rich-text">
-                        {{ object.current.private_content|bleach|safe }}
+                        {{ object.current.private_content|nh3|safe }}
                     </div>
                     {% for file in object.current.files.all %}
                         {% if forloop.first %}
diff --git a/hypha/apply/projects/templates/application_projects/vendor_detail.html b/hypha/apply/projects/templates/application_projects/vendor_detail.html
index c45bf7c16..9fb4d2f0b 100644
--- a/hypha/apply/projects/templates/application_projects/vendor_detail.html
+++ b/hypha/apply/projects/templates/application_projects/vendor_detail.html
@@ -1,5 +1,5 @@
 {% extends "base-apply.html" %}
-{% load bleach_tags i18n approval_tools heroicons %}
+{% load nh3_tags i18n approval_tools heroicons %}
 {% user_can_edit_project object request.user as editable %}
 {% block title %}{% trans "Contracting Information for" %} {{ project.title }} {% endblock %}
 
@@ -31,7 +31,7 @@
     <div class="rich-text rich-text--answers">
         {% for group in vendor_detailed_response.values %}
             {% if group.title %}
-                <h1>{{ group.title|bleach }}</h4>
+                <h1>{{ group.title|nh3 }}</h4>
             {% endif %}
             {% for question, answer in group.questions %}
                 <h5>{{ question }}</h5>
@@ -44,7 +44,7 @@
                         </div>
                     </div>
                 {% else %}
-                    <p>{% if answer == True or answer == False %}{{ answer|yesno:"Yes,No" }}{% else %}{% if answer %}{{ answer|bleach }}{% else %}-{% endif %}{% endif %}</p>
+                    <p>{% if answer == True or answer == False %}{{ answer|yesno:"Yes,No" }}{% else %}{% if answer %}{{ answer|nh3 }}{% else %}-{% endif %}{% endif %}</p>
                 {% endif %}
             {% endfor %}
         {% endfor %}
diff --git a/hypha/apply/review/templates/review/render_scored_answer_field.html b/hypha/apply/review/templates/review/render_scored_answer_field.html
index a3ac5718d..6b94842c4 100644
--- a/hypha/apply/review/templates/review/render_scored_answer_field.html
+++ b/hypha/apply/review/templates/review/render_scored_answer_field.html
@@ -1,8 +1,8 @@
-{% load bleach_tags %}
+{% load nh3_tags %}
 {% block data_display %}
     <div>
         <h5>{{ value.field_label }}</h5>
         <div>{{ score }}</div>
-        <div>{{ comment|bleach }}</div>
+        <div>{{ comment|nh3 }}</div>
     </div>
 {% endblock %}
diff --git a/hypha/apply/review/templates/review/review_detail.html b/hypha/apply/review/templates/review/review_detail.html
index 706828fd4..535f6ff36 100644
--- a/hypha/apply/review/templates/review/review_detail.html
+++ b/hypha/apply/review/templates/review/review_detail.html
@@ -1,5 +1,5 @@
 {% extends "base-apply.html" %}
-{% load i18n bleach_tags submission_tags heroicons %}
+{% load i18n nh3_tags submission_tags heroicons %}
 {% block title %}{% trans "Review for" %} {{ review.submission.title }}{% endblock %}
 {% block content %}
 
diff --git a/hypha/apply/review/templates/review/review_list.html b/hypha/apply/review/templates/review/review_list.html
index 3cfdd3298..272f0b560 100644
--- a/hypha/apply/review/templates/review/review_list.html
+++ b/hypha/apply/review/templates/review/review_list.html
@@ -1,5 +1,5 @@
 {% extends "base-apply.html" %}
-{% load i18n bleach_tags review_tags workflow_tags %}
+{% load i18n nh3_tags review_tags workflow_tags %}
 
 {% block title %}{% trans "Reviews" %}{% endblock %}
 
@@ -28,7 +28,7 @@
                         {% elif answers.question == "Opinions"%}
                             <td class="reviews-list__td">{{ answer }}</td>
                         {% else %}
-                            <td class="reviews-list__td">{{ answer|bleach }}</td>
+                            <td class="reviews-list__td">{{ answer|nh3 }}</td>
                         {% endif %}
                     {% endfor %}
                 </tr>
diff --git a/hypha/apply/stream_forms/blocks.py b/hypha/apply/stream_forms/blocks.py
index 70d264dd5..13dc30015 100644
--- a/hypha/apply/stream_forms/blocks.py
+++ b/hypha/apply/stream_forms/blocks.py
@@ -1,5 +1,5 @@
 # Credit to https://github.com/BertrandBordage for initial implementation
-import bleach
+import nh3
 from anyascii import anyascii
 from dateutil.parser import isoparse, parse
 from django import forms
@@ -11,7 +11,7 @@ from django.utils.encoding import force_str
 from django.utils.html import conditional_escape
 from django.utils.text import slugify
 from django.utils.translation import gettext_lazy as _
-from django_bleach.templatetags.bleach_tags import bleach_value
+from django_nh3.templatetags.nh3_tags import nh3_value
 from wagtail.blocks import (
     BooleanBlock,
     CharBlock,
@@ -89,7 +89,7 @@ class FormFieldBlock(StructBlock):
         }
 
     def prepare_data(self, value, data, serialize=False):
-        return bleach_value(str(data))
+        return nh3_value(str(data))
 
     def render(self, value, context):
         data = context.get("data")
@@ -141,7 +141,7 @@ class CharFieldBlock(OptionalFormFieldBlock):
     def get_searchable_content(self, value, data):
         # CharField acts as a fallback. Force data to string
         data = str(data)
-        return bleach.clean(data or "", tags=[], strip=True)
+        return nh3.clean(data or "", tags=set())
 
 
 class MultiInputCharFieldBlock(CharFieldBlock):
@@ -165,7 +165,7 @@ class TextFieldBlock(OptionalFormFieldBlock):
         template = "stream_forms/render_unsafe_field.html"
 
     def get_searchable_content(self, value, data):
-        return bleach.clean(data or "", tags=[], strip=True)
+        return nh3.clean(data or "", tags=set())
 
 
 class NumberFieldBlock(OptionalFormFieldBlock):
diff --git a/hypha/apply/stream_forms/templates/stream_forms/render_markdown_field.html b/hypha/apply/stream_forms/templates/stream_forms/render_markdown_field.html
index f9b586177..4ef393bd4 100644
--- a/hypha/apply/stream_forms/templates/stream_forms/render_markdown_field.html
+++ b/hypha/apply/stream_forms/templates/stream_forms/render_markdown_field.html
@@ -1,8 +1,8 @@
 {% extends "stream_forms/render_field.html" %}
-{% load bleach_tags markdown_tags %}
+{% load nh3_tags markdown_tags %}
 {% block data_display %}
     {% if data %}
-        {{ data|markdown|bleach }}
+        {{ data|markdown|nh3 }}
     {% else %}
         {{ block.super }}
     {% endif %}
diff --git a/hypha/apply/stream_forms/templates/stream_forms/render_unsafe_field.html b/hypha/apply/stream_forms/templates/stream_forms/render_unsafe_field.html
index 9725f800f..3138a4d25 100644
--- a/hypha/apply/stream_forms/templates/stream_forms/render_unsafe_field.html
+++ b/hypha/apply/stream_forms/templates/stream_forms/render_unsafe_field.html
@@ -1,13 +1,13 @@
 {% extends "stream_forms/render_field.html" %}
-{% load bleach_tags %}
+{% load nh3_tags %}
 {% block data_display %}
     {% if data %}
         {% if value.format == 'url' %}
-            <a class="link" href="{{ data|bleach }}" target="_blank" rel="noopener noreferrer">{{ data|bleach }}</a>
+            <a class="link" href="{{ data|nh3 }}" target="_blank" rel="noopener noreferrer">{{ data|nh3 }}</a>
         {% elif value.format == 'email' %}
-            <a class="u-email" href="mailto:{{ data|bleach }}">{{ data|bleach }}</a>
+            <a class="u-email" href="mailto:{{ data|nh3 }}">{{ data|nh3 }}</a>
         {% else %}
-            <p>{{ data|bleach }}</p>
+            <p>{{ data|nh3 }}</p>
         {% endif %}
     {% else %}
         {{ block.super }}
diff --git a/hypha/apply/templates/forms/includes/field.html b/hypha/apply/templates/forms/includes/field.html
index b9dec1d83..f4c2802c1 100644
--- a/hypha/apply/templates/forms/includes/field.html
+++ b/hypha/apply/templates/forms/includes/field.html
@@ -1,5 +1,5 @@
 {% load i18n util_tags %}
-{% load bleach_tags markdown_tags heroicons %}
+{% load nh3_tags markdown_tags heroicons %}
 {% with widget_type=field|widget_type field_type=field|field_type %}
 
     <div class="form__group {{ field.id_for_label }} form__group--{{ widget_type }} {% if widget_type == 'checkbox_input' %} form__group--checkbox{% endif %}{% if widget_type == 'clearable_file_input' or widget_type == 'multi_file_input' or widget_type == 'single_file_field_widget' or widget_type == 'multi_file_field_widget' %} form__group--file{% endif %}{% if field.help_text %} form__group--wrap{% endif %}{% if field.errors %} form__error{% endif %}{% if is_application and field.field.group_number > 1 %} field-group field-group-{{ field.field.group_number }}{% endif %}{% if is_application and field.field.grouper_for %} form-fields-grouper{% endif %}"{% if is_application and field.field.grouper_for %}data-grouper-for="{{ field.field.grouper_for }}" data-toggle-on="{{ field.field.choices.0.0 }}" data-toggle-off="{{ field.field.choices.1.0 }}"{% endif %}{% if is_application and field.field.group_number > 1 %} data-hidden="{% if not show_all_group_fields and not field.field.visible %}true{% else %}false{% endif %}" data-required="{{ field.field.required_when_visible }}"{% endif %}{% if field.field.word_limit %} data-word-limit="{{ field.field.word_limit }}"{% endif %}>
@@ -34,7 +34,7 @@
         {% endif %}
 
         {% if field.help_text %}
-            <div class="form__help prose prose-sm">{{ field.help_text|markdown|bleach }}</div>
+            <div class="form__help prose prose-sm">{{ field.help_text|markdown|nh3 }}</div>
         {% endif %}
 
         {% if field.field.help_link %}
diff --git a/hypha/apply/utils/blocks.py b/hypha/apply/utils/blocks.py
index 7f74b0c50..baee0b40e 100644
--- a/hypha/apply/utils/blocks.py
+++ b/hypha/apply/utils/blocks.py
@@ -1,6 +1,6 @@
 from collections import Counter
 
-import bleach
+import nh3
 from django.core.exceptions import ValidationError
 from django.forms.utils import ErrorList
 from django.utils.safestring import mark_safe
@@ -48,7 +48,7 @@ class RichTextFieldBlock(TextFieldBlock):
         icon = "form"
 
     def get_searchable_content(self, value, data):
-        return bleach.clean(data or "", tags=[], strip=True)
+        return nh3.clean(data or "", tags=set())
 
     def no_response(self):
         return "<p>-</p>"
@@ -64,7 +64,7 @@ class MarkdownTextFieldBlock(TextFieldBlock):
         template = "stream_forms/render_markdown_field.html"
 
     def get_searchable_content(self, value, data):
-        return bleach.clean(data or "", tags=[], strip=True)
+        return nh3.clean(data or "", tags=set())
 
     def no_response(self):
         return "<p>-</p>"
diff --git a/hypha/home/templates/apply_home/includes/apply_listing.html b/hypha/home/templates/apply_home/includes/apply_listing.html
index 83d8cbf39..d4b4ac9e4 100644
--- a/hypha/home/templates/apply_home/includes/apply_listing.html
+++ b/hypha/home/templates/apply_home/includes/apply_listing.html
@@ -1,4 +1,4 @@
-{% load i18n bleach_tags wagtailcore_tags markdown_tags heroicons %}
+{% load i18n nh3_tags wagtailcore_tags markdown_tags heroicons %}
 
 {% if page.open_round and page.list_on_front_page %}
   <div class="flex justify-between items-center px-4 py-8 gap-4 hover:bg-slate-50 transition-colors">
@@ -17,7 +17,7 @@
 
       {% if page.description %}
         <p>
-          {{ page.description|markdown|bleach }}
+          {{ page.description|markdown|nh3 }}
         </p>
       {% endif %}
     </div>
diff --git a/hypha/public/news/templates/news/news_index.html b/hypha/public/news/templates/news/news_index.html
new file mode 100644
index 000000000..1cdeb6220
--- /dev/null
+++ b/hypha/public/news/templates/news/news_index.html
@@ -0,0 +1,56 @@
+{% extends "base.html" %}
+{% load wagtailcore_tags wagtailimages_tags static markdown_tags nh3_tags %}
+{% block feedlinks %}<link rel="alternate" type="application/rss+xml" title="{{ page.title }}" href="{% url "news_feed" %}">{% endblock %}
+{% block body_class %}light-grey-bg{% endblock %}
+
+{% block content %}
+    <div class="wrapper wrapper--small wrapper--inner-space-medium">
+
+        {% if page.introduction %}
+            <h4 class="heading heading--listings-introduction">{{ page.introduction|markdown|nh3 }}</h4>
+        {% endif %}
+
+        <form class="form" method="GET">
+            <div class="form__select form__select--narrow form__select--inline">
+                <select name="news_type">
+                    <option value="">All</option>
+                    {% for news_type in news_types %}
+                        <option value="{{ news_type.0 }}" {% if request.GET.news_type == news_type.0|slugify %}selected="selected"{% endif %}>{{ news_type.1 }}</option>
+                    {% endfor %}
+                </select>
+            </div>
+            <button class="link link--button link--button__stretch" type="submit">Filter</button>
+        </form>
+
+        {% if news %}
+            <div class="wrapper wrapper--listings wrapper--top-space">
+                {% for n in news %}
+                    <a class="listing" href="{% pageurl n %}">
+                        {% if n.listing_image %}
+                            {% image n.listing_image fill-450x300 %}
+                        {% endif %}
+                        <h4 class="listing__title" role="listitem">
+                            {{ n.listing_title|default:n.title }}
+                        </h4>
+                        {% if n.listing_summary or n.introduction %}
+                            <h6 class="listing__teaser">{{ n.listing_summary|default:n.introduction }}</h6>
+                        {% endif %}
+                        <span class="listing__meta">
+                            {{ n.display_date|date:"SHORT_DATE_FORMAT" }}
+                            {% if n.authors.all %}
+                                | By:
+                                {% for author in n.authors.all %}
+                                    {{ author.author }}
+                                {% endfor %}
+                            {% endif %}
+                        </span>
+                    </a>
+                {% endfor %}
+            </div>
+            {% include "includes/pagination.html" with paginator_page=news %}
+        {% else %}
+            {# no items #}
+        {% endif %}
+
+    </div>
+{% endblock %}
diff --git a/hypha/settings/base.py b/hypha/settings/base.py
index 6c4eb4854..e5fb60735 100644
--- a/hypha/settings/base.py
+++ b/hypha/settings/base.py
@@ -383,9 +383,9 @@ SOCIAL_AUTH_PIPELINE = (
     "hypha.apply.users.pipeline.make_otf_staff",
 )
 
-# Bleach Settings
+# NH3 Settings
 
-BLEACH_ALLOWED_TAGS = [
+NH3_ALLOWED_TAGS = [
     "a",
     "b",
     "big",
@@ -426,18 +426,20 @@ BLEACH_ALLOWED_TAGS = [
     "tr",
     "ul",
 ]
-BLEACH_ALLOWED_ATTRIBUTES = [
-    "class",
-    "colspan",
-    "href",
-    "rowspan",
-    "target",
-    "title",
-    "width",
-]
-BLEACH_ALLOWED_STYLES = []
-BLEACH_STRIP_TAGS = True
-BLEACH_STRIP_COMMENTS = True
+
+NH3_ALLOWED_ATTRIBUTES = {
+    "*": [
+        "class",
+        "colspan",
+        "href",
+        "rowspan",
+        "target",
+        "title",
+        "width",
+    ]
+}
+
+NH3_STRIP_COMMENTS = True
 
 
 # Hijack Settings
diff --git a/hypha/settings/django.py b/hypha/settings/django.py
index 40e092d9f..3f3cac9bf 100644
--- a/hypha/settings/django.py
+++ b/hypha/settings/django.py
@@ -57,7 +57,7 @@ INSTALLED_APPS = [
     "django_filters",
     "django_select2",
     "addressfield",
-    "django_bleach",
+    "django_nh3",
     "django_fsm",
     "django_pwned_passwords",
     "django_slack",
diff --git a/requirements.txt b/requirements.txt
index 7ce442991..1ab5b86ba 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,7 +10,7 @@ click==8.1.7
 dj-database-url==2.1.0
 django-anymail==10.2
 django-basic-auth-ip-whitelist==0.5
-django-bleach==3.1.0
+django-nh3==0.1.1
 django-countries==7.5.1
 django-elevate==2.0.3
 django-extensions==3.2.3
-- 
GitLab