Skip to content

Commit 68887d1

Browse files
author
Feanil Patel
authored
Merge pull request from GHSA-qv6c-367r-3w6q
feat: sanitize HTML tags to prevent XSS vulnerabilities
2 parents d386716 + 53c4482 commit 68887d1

File tree

7 files changed

+149
-14
lines changed

7 files changed

+149
-14
lines changed

Changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Drag and Drop XBlock changelog
22
==============================
33

4+
Version 3.0.0 (2022-11-18)
5+
---------------------------
6+
7+
* Sanitize HTML tags to prevent XSS vulnerabilities.
8+
BREAKING CHANGE: Disallowed HTML tags (e.g. `<script>`) will no longer be rendered in LMS and Studio.
9+
410
Version 2.7.0 (2022-11-15)
511
---------------------------
612

drag_and_drop_v2/drag_and_drop_v2.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from .default_data import DEFAULT_DATA
3232
from .utils import (
3333
Constants, SHOWANSWER, DummyTranslationService, FeedbackMessage,
34-
FeedbackMessages, ItemStats, StateMigration, _clean_data, _
34+
FeedbackMessages, ItemStats, StateMigration, _clean_data, _, sanitize_html
3535
)
3636

3737
# Globals ###########################################################
@@ -388,11 +388,12 @@ def items_without_answers():
388388
item['expandedImageURL'] = self._expand_static_url(image_url)
389389
else:
390390
item['expandedImageURL'] = ''
391+
item['displayName'] = sanitize_html(item.get('displayName', ''))
391392
return items
392393

393394
return {
394395
"block_id": six.text_type(self.scope_ids.usage_id),
395-
"display_name": self.display_name,
396+
"display_name": sanitize_html(self.display_name),
396397
"type": self.CATEGORY,
397398
"weight": self.weight,
398399
"mode": self.mode,
@@ -407,9 +408,9 @@ def items_without_answers():
407408
"display_zone_borders": self.data.get('displayBorders', False),
408409
"display_zone_borders_dragging": self.data.get('displayBordersDragging', False),
409410
"items": items_without_answers(),
410-
"title": self.display_name,
411+
"title": sanitize_html(self.display_name),
411412
"show_title": self.show_title,
412-
"problem_text": self.question_text,
413+
"problem_text": sanitize_html(self.question_text),
413414
"show_problem_header": self.show_question_header,
414415
"target_img_expanded_url": self.target_img_expanded_url,
415416
"target_img_description": self.target_img_description,
@@ -667,7 +668,7 @@ def show_answer(self, data, suffix=''):
667668
except (TypeError, AttributeError):
668669
logger.debug('Unable to perform URL substitution on the explanation: %s', explanation)
669670

670-
answer['explanation'] = explanation
671+
answer['explanation'] = sanitize_html(explanation)
671672
return answer
672673

673674
@XBlock.json_handler
@@ -885,7 +886,7 @@ def _present_feedback(feedback_messages):
885886
Transforms feedback messages into format expected by frontend code
886887
"""
887888
return [
888-
{"message": msg.message, "message_class": msg.message_class}
889+
{"message": sanitize_html(msg.message), "message_class": msg.message_class}
889890
for msg in feedback_messages
890891
if msg.message
891892
]
@@ -1195,7 +1196,14 @@ def zones(self):
11951196
"""
11961197
# Convert zone data from old to new format if necessary
11971198
migrator = StateMigration(self)
1198-
return [migrator.apply_zone_migrations(zone) for zone in self.data.get('zones', [])]
1199+
migrated_zones = []
1200+
1201+
for zone in self.data.get('zones', []):
1202+
result = migrator.apply_zone_migrations(zone)
1203+
result['title'] = sanitize_html(result.get('title', ''))
1204+
migrated_zones.append(result)
1205+
1206+
return migrated_zones
11991207

12001208
def _get_zone_by_uid(self, uid):
12011209
"""

drag_and_drop_v2/public/js/drag_and_drop_edit.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,8 +448,8 @@ function DragAndDropEditBlock(runtime, element, params) {
448448
_fn.build.$el.zonesPreview.append(
449449
_fn.tpl.zoneElement({
450450
uid: zoneObj.uid,
451-
title: gettext(zoneObj.title),
452-
description: gettext(zoneObj.description),
451+
title: Handlebars.Utils.escapeExpression(gettext(zoneObj.title)),
452+
description: Handlebars.Utils.escapeExpression(gettext(zoneObj.description)),
453453
x_percent: (+zoneObj.x) / imgWidth * 100,
454454
y_percent: (+zoneObj.y) / imgHeight * 100,
455455
width_percent: (+zoneObj.width) / imgWidth * 100,

drag_and_drop_v2/utils.py

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@
66
import re
77
from collections import namedtuple
88

9-
from bleach.sanitizer import Cleaner
9+
import bleach
10+
11+
try:
12+
from bleach.css_sanitizer import CSSSanitizer
13+
except (ImportError, ModuleNotFoundError):
14+
# pylint: disable=fixme
15+
# TODO: The bleach library changes the way CSS Styles are cleaned in version 5.0.0. The edx-platform uses version
16+
# 4.1.0 in Maple, so this import is handled within a try block. This can be removed for the Nutmeg release.
17+
CSSSanitizer = None
1018

1119

1220
def _(text):
@@ -24,11 +32,124 @@ def ngettext_fallback(text_singular, text_plural, number):
2432

2533
def _clean_data(data):
2634
""" Remove html tags and extra white spaces e.g newline, tabs etc from provided data """
27-
cleaner = Cleaner(tags=[], strip=True)
35+
cleaner = bleach.Cleaner(tags=[], strip=True)
2836
cleaned_text = " ".join(re.split(r"\s+", cleaner.clean(data), flags=re.UNICODE)).strip()
2937
return cleaned_text
3038

3139

40+
ALLOWED_TAGS = bleach.ALLOWED_TAGS + [
41+
'br',
42+
'caption',
43+
'dd',
44+
'del',
45+
'div',
46+
'dl',
47+
'dt',
48+
'h1',
49+
'h2',
50+
'h3',
51+
'h4',
52+
'h5',
53+
'h6',
54+
'hr',
55+
'img',
56+
'p',
57+
'pre',
58+
's',
59+
'strike',
60+
'span',
61+
'sub',
62+
'sup',
63+
'table',
64+
'tbody',
65+
'td',
66+
'tfoot',
67+
'th',
68+
'thead',
69+
'tr',
70+
'u',
71+
]
72+
ALLOWED_ATTRIBUTES = {
73+
'*': ['class', 'style', 'id'],
74+
'a': ['href', 'title', 'target', 'rel'],
75+
'abbr': ['title'],
76+
'acronym': ['title'],
77+
'audio': ['controls', 'autobuffer', 'autoplay', 'src'],
78+
'img': ['src', 'alt', 'title', 'width', 'height'],
79+
'table': ['border', 'cellspacing', 'cellpadding'],
80+
'td': ['style', 'scope'],
81+
}
82+
ALLOWED_STYLES = [
83+
"azimuth",
84+
"background-color",
85+
"border-bottom-color",
86+
"border-collapse",
87+
"border-color",
88+
"border-left-color",
89+
"border-right-color",
90+
"border-top-color",
91+
"clear",
92+
"color",
93+
"cursor",
94+
"direction",
95+
"display",
96+
"elevation",
97+
"float",
98+
"font",
99+
"font-family",
100+
"font-size",
101+
"font-style",
102+
"font-variant",
103+
"font-weight",
104+
"height",
105+
"letter-spacing",
106+
"line-height",
107+
"overflow",
108+
"pause",
109+
"pause-after",
110+
"pause-before",
111+
"pitch",
112+
"pitch-range",
113+
"richness",
114+
"speak",
115+
"speak-header",
116+
"speak-numeral",
117+
"speak-punctuation",
118+
"speech-rate",
119+
"stress",
120+
"text-align",
121+
"text-decoration",
122+
"text-indent",
123+
"unicode-bidi",
124+
"vertical-align",
125+
"voice-family",
126+
"volume",
127+
"white-space",
128+
"width",
129+
]
130+
131+
132+
def sanitize_html(raw_body: str) -> str:
133+
"""
134+
Remove not allowed HTML tags to mitigate XSS vulnerabilities.
135+
"""
136+
bleach_options = dict(
137+
tags=ALLOWED_TAGS,
138+
protocols=bleach.ALLOWED_PROTOCOLS,
139+
strip=True,
140+
attributes=ALLOWED_ATTRIBUTES,
141+
)
142+
if CSSSanitizer:
143+
bleach_options['css_sanitizer'] = CSSSanitizer()
144+
else:
145+
# pylint: disable=fixme
146+
# TODO: This is maintaining backward compatibility with bleach 4.1.0 used in Maple release of edx-platform.
147+
# This can be removed for the Nutmeg release which uses bleach 5.0.0
148+
bleach_options['styles'] = ALLOWED_STYLES
149+
150+
return bleach.clean(raw_body, **bleach_options)
151+
152+
32153
class DummyTranslationService:
33154
"""
34155
Dummy drop-in replacement for i18n XBlock service

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def package_data(pkg, root_list):
2323

2424
setup(
2525
name='xblock-drag-and-drop-v2',
26-
version='2.7.0',
26+
version='3.0.0',
2727
description='XBlock - Drag-and-Drop v2',
2828
packages=['drag_and_drop_v2'],
2929
install_requires=[

tests/integration/test_custom_data_render.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def test_items_rendering(self):
2222
self.assertEqual(len(items), 3)
2323
self.assertIn('<b>1</b>', self.get_element_html(items[0]))
2424
self.assertIn('<i>2</i>', self.get_element_html(items[1]))
25-
self.assertIn('<span style="color:red">X</span>', self.get_element_html(items[2]))
25+
self.assertIn('<span style="color: red;">X</span>', self.get_element_html(items[2]))
2626

2727
def test_html_title_renders_properly(self):
2828
"""

tests/integration/test_title_and_question.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_problem_parameters(self, problem_text, show_problem_header):
4040
('plain shown', 'title1', True),
4141
('plain hidden', 'title2', False),
4242
('html shown', 'title with <i>HTML</i>', True),
43-
('html hidden', '<span style="color:red">Title: HTML?</span>', False)
43+
('html hidden', '<span style="color: red;">Title: HTML?</span>', False)
4444
)
4545
def test_title_parameters(self, _, display_name, show_title):
4646
const_page_name = 'Test show title parameter'

0 commit comments

Comments
 (0)