diff --git a/tests/test_markdown_filters.py b/tests/test_markdown_filters.py
new file mode 100644
index 000000000..94c7b2c6a
--- /dev/null
+++ b/tests/test_markdown_filters.py
@@ -0,0 +1,102 @@
+from django.test import TestCase
+
+from web.templatetags.markdown_filters import markdownify_sanitized
+
+
+class MarkdownFiltersSanitizationTests(TestCase):
+ """
+ Regression tests covering the sanitizer boundary around the markdownify_sanitized function.
+ """
+
+ def test_markdownify_sanitized_strips_scripts_and_dangerous_tags(self):
+ """Ensure ')
+ self.assertNotIn("."
+ sanitized = markdownify_sanitized(complex_payload)
+ self.assertIn("link", sanitized)
+ self.assertNotIn("
+
{{ form.media }}
{% endblock extra_head %}
{% block content %}
@@ -102,7 +103,8 @@
'preview', 'side-by-side', 'fullscreen', '|',
'guide'
],
- theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light'
+ theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
+ previewRender: (plainText) => sanitizedPreviewRender(plainText, '{% url "markdownx_markdownify" %}')
});
// Store the editor instance
diff --git a/web/templates/success_stories/detail.html b/web/templates/success_stories/detail.html
index 9b150708d..7acf699ed 100644
--- a/web/templates/success_stories/detail.html
+++ b/web/templates/success_stories/detail.html
@@ -1,5 +1,6 @@
{% extends "base.html" %}
+{% load markdown_filters %}
{% load static %}
{% block title %}{{ success_story.title }}{% endblock %}
@@ -55,7 +56,7 @@ {{ success_story.title }}
{% endif %}
-
{{ success_story.content|safe }}
+ {{ success_story.content|markdown }}
Share this success story
diff --git a/web/templates/teach.html b/web/templates/teach.html
index 02c111c10..be15e930f 100644
--- a/web/templates/teach.html
+++ b/web/templates/teach.html
@@ -17,6 +17,7 @@
+
{{ form.media }}
{% endblock extra_head %}
{% block content %}
@@ -84,7 +85,8 @@ Create Your Course
minHeight: '100px',
maxHeight: '400px',
placeholder: 'Describe your course...',
- autoDownloadFontAwesome: false
+ autoDownloadFontAwesome: false,
+ previewRender: (plainText) => sanitizedPreviewRender(plainText, '{% url "markdownx_markdownify" %}')
});
editors.set(textarea.name, editor);
editor.codemirror.on('change', () => {
diff --git a/web/templatetags/markdown_filters.py b/web/templatetags/markdown_filters.py
index ecda623a4..f05621bce 100644
--- a/web/templatetags/markdown_filters.py
+++ b/web/templatetags/markdown_filters.py
@@ -1,11 +1,69 @@
+from typing import Any, Optional
+
+import bleach
from django import template
from django.utils.safestring import mark_safe
from markdownx.utils import markdownify
register = template.Library()
+SAFE_TAGS = {
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6",
+ "p",
+ "br",
+ "hr",
+ "strong",
+ "em",
+ "b",
+ "i",
+ "u",
+ "del",
+ "ul",
+ "ol",
+ "li",
+ "a",
+ "pre",
+ "code",
+ "blockquote",
+ "table",
+ "thead",
+ "tbody",
+ "tr",
+ "th",
+ "td",
+ "img",
+}
+
+SAFE_ATTRIBUTES = {
+ "a": {"href", "title"},
+ "img": {"src", "alt", "title"},
+ "code": {"class"},
+ "td": {"align"},
+ "th": {"align"},
+}
+
+ALLOWED_PROTOCOLS = {"http", "https", "mailto"}
+
+
+def markdownify_sanitized(content: Optional[str]) -> str:
+ """
+ Custom markdownify function that sanitizes the output HTML using bleach.
+ This is used both for template filters and for the markdownx AJAX preview.
+ """
+ if not content:
+ return ""
+
+ return bleach.clean(
+ markdownify(content), tags=SAFE_TAGS, attributes=SAFE_ATTRIBUTES, protocols=ALLOWED_PROTOCOLS, strip=True
+ )
+
@register.filter
-def markdown(text):
- """Convert markdown text to HTML."""
- return mark_safe(markdownify(text))
+def markdown(text: str) -> Any:
+ """Convert markdown text to sanitized HTML."""
+ return mark_safe(markdownify_sanitized(text))