diff --git a/course/page/code.py b/course/page/code.py
index 2978954c56cbb54aecd6e7a07f9411efa10a8fb7..ecb14118c42e2584663f033d2e33edd9c0c71842 100644
--- a/course/page/code.py
+++ b/course/page/code.py
@@ -894,12 +894,9 @@ class PythonCodeQuestion(PageBaseWithTitle, PageBaseWithValue):
                     if name in ["type"]:
                         return True
                     elif name == "src":
-                        if is_allowed_data_uri([
-                                "audio/wav",
-                                ], value):
-                            return bleach.sanitizer.VALUE_SAFE
-                        else:
-                            return False
+                        return is_allowed_data_uri([
+                            "audio/wav",
+                            ], value)
                     else:
                         return False
 
@@ -907,11 +904,10 @@ class PythonCodeQuestion(PageBaseWithTitle, PageBaseWithValue):
                     if name in ["alt", "title"]:
                         return True
                     elif name == "src":
-                        if is_allowed_data_uri([
-                                "image/png",
-                                "image/jpeg",
-                                ], value):
-                            return bleach.sanitizer.VALUE_SAFE
+                        return is_allowed_data_uri([
+                            "image/png",
+                            "image/jpeg",
+                            ], value)
                     else:
                         return False
 
@@ -925,7 +921,11 @@ class PythonCodeQuestion(PageBaseWithTitle, PageBaseWithValue):
                             "audio": filter_audio_attributes,
                             "source": filter_source_attributes,
                             "img": filter_img_attributes,
-                            })
+                            },
+
+                        # Fixed https://github.com/inducer/relate/issues/435
+                        # Ref: https://github.com/mozilla/bleach/issues/348
+                        protocols=["data"])
 
             bulk_feedback_bits.extend(
                     sanitize(snippet) for snippet in response.html)
diff --git a/requirements.txt b/requirements.txt
index 3dc2f8e9ad884e797326ec402c9b7d914898130c..063206e95bdf693d877c538977adfd9d1ef22d24 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -96,9 +96,7 @@ git+https://github.com/celery/django-celery.git@6232c79557517afa74967fcb980c0db2
 django_select2>=5.5.0
 
 # To sanitize HTML generated by user code
-# for https://github.com/mozilla/bleach/pull/346
-git+https://github.com/inducer/bleach@prevent-attribute-value-filtering
-#bleach>=2.0,<3
+bleach>=2.0,<3
 
 # Custom user migration created using
 # https://bitbucket.org/spookylukey/django_custom_user_migration
diff --git a/tests/test_pages/test_code.py b/tests/test_pages/test_code.py
index 7fc9b02ce4590d98c5bedcfbdc6194dda4620c30..b108e98eab666a87fb39259f8aca669d45b2b531 100644
--- a/tests/test_pages/test_code.py
+++ b/tests/test_pages/test_code.py
@@ -783,36 +783,38 @@ class CodeQuestionTest(SingleCoursePageSandboxTestBaseMixin,
 
     def test_html_audio(self):
         b64_data = "T2dnUwACAAAAAAAAAAA+HAAAAAAAAGyawCEBQGZpc2h"
-        audio1 = (
+        audio_valid = (
             '<audio controls><source src="data:audio/wav;base64,'
             '%s" type="audio/wav">'
             '</audio>' % b64_data)
-        audio1_1 = (
+        audio_invalid1 = (
             '<audio control><source src="data:audio/wav;base64,'
             '%s" type="audio/wav">'
             '</audio>' % b64_data)
-        audio1_2 = (
+        audio_invalid2 = (
             '<audio controls><source href="data:audio/wav;base64,'
             '%s" type="audio/wav">'
             '</audio>' % b64_data)
-        audio2 = (
+        audio_invalid3 = (
             '<audio><source src="data:audio/wav;base64,'
             '%s" type="audio/wav">'
             '</audio>' % b64_data)
-        audio3 = (
+        audio_invalid4 = (
             '<audio controls><source src="data:audio/ogg;base64,'
             '%s" type="audio/ogg">'
             '</audio>' % b64_data)
-        audio4 = (
+        audio_invalid5 = (
             '<audio controls><source src="hosse.wav" type="audio/wav">'
             '</audio>')
 
-        html = [audio1, audio1_1, audio1_2, audio2, audio3, audio4]
+        html = [audio_valid, audio_invalid1, audio_invalid2, audio_invalid3,
+                audio_invalid4, audio_invalid5]
 
         self.assert_runpy_result_and_response(
             "user_error",
-            expected_msgs=[audio1],
-            not_execpted_msgs=[audio1_1, audio1_2, audio2, audio3, audio4],
+            expected_msgs=[audio_valid],
+            not_execpted_msgs=[audio_invalid1, audio_invalid2, audio_invalid3,
+                               audio_invalid4, audio_invalid5],
             html=html,
             in_html=True
         )
@@ -823,34 +825,63 @@ class CodeQuestionTest(SingleCoursePageSandboxTestBaseMixin,
             "6XZn90AAAAJcEhZcwAADsIAAA7CARUoSoAAAAAMSURBVBjTYzjAcAAAAwQBgXn"
             "6PNcAAAAASUVORK5CYII=")
 
-        img1 = (
+        img_valid = (
             '<img src="data:image/png;base64,%s" alt="test img" '
             'title="test image">' % b64_data)
 
-        img1_1 = (
+        img_invalid1 = (
             '<img src="data:image/png;base64,%s" '
             'alt="test img" '
             'width="126" '
             'height="44">' % b64_data)
 
-        img1_2 = (
+        img_invalid2 = (
             '<img href="data:image/png;base64,%s" '
             'alt="test img" title="test image">' % b64_data)
 
-        img2 = (
+        img_invalid3 = (
             '<img src="data:image/bmp;base64,%s" '
             'alt="test img" title="test image">' % b64_data)
 
-        html = [img1, img1_1, img1_2, img2]
+        html = [img_valid, img_invalid1, img_invalid2, img_invalid3]
 
         self.assert_runpy_result_and_response(
             "user_error",
-            expected_msgs=[img1],
-            # not_execpted_msgs=[img1_1, img1_2, img2],
+            expected_msgs=[img_valid],
+            not_execpted_msgs=[img_invalid1, img_invalid2, img_invalid3],
             html=html,
             in_html=True,
         )
 
+    def test_html_with_data_protocol_for_other_tags_sanitized(self):
+        # Fixed https://github.com/inducer/relate/issues/435
+        # Ref: https://github.com/mozilla/bleach/issues/348
+        b64_data = ("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAQMAAAB=")
+
+        html_strings = [
+            '<a src="data:,Hello%2C%20Evil%20World!"></a>',
+            '<a href="data:,Hello%2C%20Evil%20World!"></a>',
+            '<a src="data:text/html;base64,%s"</a>' % b64_data,
+            '<a src="data:text/html;base64,%s"</a>' % b64_data,
+
+            '<script src="data:text/html,<script>alert("Evil");</script>"',
+            '<script href="data:text/html,<script>alert("Evil");</script>"',
+            '<script src="data:text/html;base64,%s"</script>' % b64_data,
+            '<script href="data:text/html;base64,%s"</script>' % b64_data,
+
+            '<style src="data:,Evilcss">',
+            '<style href="data:,Evilcss">',
+            '<style src="data:image/png;base64,%s">' % b64_data,
+            '<style href="data:image/png;base64,%s">' % b64_data,
+        ]
+
+        self.assert_runpy_result_and_response(
+            "user_error",
+            not_execpted_msgs=html_strings + ["Evil", b64_data],
+            html=html_strings,
+            in_html=True,
+        )
+
     def test_html_non_text_bleached_in_feedback(self):
         self.assert_runpy_result_and_response(
             "user_error",