Add template escaping in forums, progress page and course container TNL-2395, TNL-2394, TNL-2393
This commit is contained in:
@@ -450,7 +450,7 @@ class SingleCohortedThreadTestCase(CohortedTestCase):
|
||||
html = response.content
|
||||
|
||||
# Verify that the group name is correctly included in the HTML
|
||||
self.assertRegexpMatches(html, r'"group_name": "student_cohort"')
|
||||
self.assertRegexpMatches(html, r'"group_name": "student_cohort"')
|
||||
|
||||
|
||||
@patch('lms.lib.comment_client.utils.requests.request')
|
||||
@@ -1152,10 +1152,10 @@ class UserProfileTestCase(ModuleStoreTestCase):
|
||||
self.assertRegexpMatches(html, r'data-num-pages="1"')
|
||||
self.assertRegexpMatches(html, r'<span>1</span> discussion started')
|
||||
self.assertRegexpMatches(html, r'<span>2</span> comments')
|
||||
self.assertRegexpMatches(html, r'"id": "{}"'.format(self.TEST_THREAD_ID))
|
||||
self.assertRegexpMatches(html, r'"title": "{}"'.format(self.TEST_THREAD_TEXT))
|
||||
self.assertRegexpMatches(html, r'"body": "{}"'.format(self.TEST_THREAD_TEXT))
|
||||
self.assertRegexpMatches(html, r'"username": "{}"'.format(self.student.username))
|
||||
self.assertRegexpMatches(html, r'"id": "{}"'.format(self.TEST_THREAD_ID))
|
||||
self.assertRegexpMatches(html, r'"title": "{}"'.format(self.TEST_THREAD_TEXT))
|
||||
self.assertRegexpMatches(html, r'"body": "{}"'.format(self.TEST_THREAD_TEXT))
|
||||
self.assertRegexpMatches(html, r'"username": "{}"'.format(self.student.username))
|
||||
|
||||
def check_ajax(self, mock_request, **params):
|
||||
response = self.get_response(mock_request, params, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
|
||||
@@ -1326,6 +1326,57 @@ class ForumFormDiscussionUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin):
|
||||
self.assertEqual(response_data["discussion_data"][0]["body"], text)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@patch('lms.lib.comment_client.utils.requests.request')
|
||||
class ForumDiscussionXSSTestCase(UrlResetMixin, ModuleStoreTestCase):
|
||||
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
def setUp(self):
|
||||
super(ForumDiscussionXSSTestCase, self).setUp()
|
||||
|
||||
username = "foo"
|
||||
password = "bar"
|
||||
|
||||
self.course = CourseFactory.create()
|
||||
self.student = UserFactory.create(username=username, password=password)
|
||||
CourseEnrollmentFactory.create(user=self.student, course_id=self.course.id)
|
||||
self.assertTrue(self.client.login(username=username, password=password))
|
||||
|
||||
@ddt.data('"><script>alert(1)</script>', '<script>alert(1)</script>', '</script><script>alert(1)</script>')
|
||||
@patch('student.models.cc.User.from_django_user')
|
||||
def test_forum_discussion_xss_prevent(self, malicious_code, mock_from_django_user, mock_request):
|
||||
"""
|
||||
Test that XSS attack is prevented
|
||||
"""
|
||||
reverse_url = "%s%s" % (reverse(
|
||||
"django_comment_client.forum.views.forum_form_discussion",
|
||||
kwargs={"course_id": unicode(self.course.id)}), '/forum_form_discussion'
|
||||
)
|
||||
# Test that malicious code does not appear in html
|
||||
url = "%s?%s=%s" % (reverse_url, 'sort_key', malicious_code)
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertNotIn(malicious_code, resp.content)
|
||||
|
||||
@ddt.data('"><script>alert(1)</script>', '<script>alert(1)</script>', '</script><script>alert(1)</script>')
|
||||
@patch('student.models.cc.User.from_django_user')
|
||||
@patch('student.models.cc.User.active_threads')
|
||||
def test_forum_user_profile_xss_prevent(self, malicious_code, mock_threads, mock_from_django_user, mock_request):
|
||||
"""
|
||||
Test that XSS attack is prevented
|
||||
"""
|
||||
mock_threads.return_value = [], 1, 1
|
||||
mock_from_django_user.return_value = Mock()
|
||||
mock_request.side_effect = make_mock_request_impl(course=self.course, text='dummy')
|
||||
|
||||
url = reverse('django_comment_client.forum.views.user_profile',
|
||||
kwargs={'course_id': unicode(self.course.id), 'user_id': str(self.student.id)})
|
||||
# Test that malicious code does not appear in html
|
||||
url_string = "%s?%s=%s" % (url, 'page', malicious_code)
|
||||
resp = self.client.get(url_string)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertNotIn(malicious_code, resp.content)
|
||||
|
||||
|
||||
class ForumDiscussionSearchUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin):
|
||||
def setUp(self):
|
||||
super(ForumDiscussionSearchUnicodeTestCase, self).setUp()
|
||||
|
||||
@@ -5,7 +5,6 @@ Views handling read (GET) requests for the Discussion tab and inline discussions
|
||||
from functools import wraps
|
||||
import json
|
||||
import logging
|
||||
import xml.sax.saxutils as saxutils
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.conf import settings
|
||||
@@ -73,13 +72,6 @@ class DiscussionTab(EnrolledTab):
|
||||
return settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE')
|
||||
|
||||
|
||||
def _attr_safe_json(obj):
|
||||
"""
|
||||
return a JSON string for obj which is safe to embed as the value of an attribute in a DOM node
|
||||
"""
|
||||
return saxutils.escape(json.dumps(obj), {'"': '"'})
|
||||
|
||||
|
||||
@newrelic.agent.function_trace()
|
||||
def make_course_settings(course, user):
|
||||
"""
|
||||
@@ -278,28 +270,28 @@ def forum_form_discussion(request, course_key):
|
||||
'course': course,
|
||||
#'recent_active_threads': recent_active_threads,
|
||||
'staff_access': bool(has_access(request.user, 'staff', course)),
|
||||
'threads': _attr_safe_json(threads),
|
||||
'threads': json.dumps(threads),
|
||||
'thread_pages': query_params['num_pages'],
|
||||
'user_info': _attr_safe_json(user_info),
|
||||
'can_create_comment': _attr_safe_json(
|
||||
'user_info': json.dumps(user_info, default=lambda x: None),
|
||||
'can_create_comment': json.dumps(
|
||||
has_permission(request.user, "create_comment", course.id)),
|
||||
'can_create_subcomment': _attr_safe_json(
|
||||
'can_create_subcomment': json.dumps(
|
||||
has_permission(request.user, "create_sub_comment", course.id)),
|
||||
'can_create_thread': has_permission(request.user, "create_thread", course.id),
|
||||
'flag_moderator': bool(
|
||||
has_permission(request.user, 'openclose_thread', course.id) or
|
||||
has_access(request.user, 'staff', course)
|
||||
),
|
||||
'annotated_content_info': _attr_safe_json(annotated_content_info),
|
||||
'annotated_content_info': json.dumps(annotated_content_info),
|
||||
'course_id': course.id.to_deprecated_string(),
|
||||
'roles': _attr_safe_json(utils.get_role_ids(course_key)),
|
||||
'roles': json.dumps(utils.get_role_ids(course_key)),
|
||||
'is_moderator': has_permission(request.user, "see_all_cohorts", course_key),
|
||||
'cohorts': course_settings["cohorts"], # still needed to render _thread_list_template
|
||||
'user_cohort': user_cohort_id, # read from container in NewPostView
|
||||
'is_course_cohorted': is_course_cohorted(course_key), # still needed to render _thread_list_template
|
||||
'sort_preference': user.default_sort_key,
|
||||
'category_map': course_settings["category_map"],
|
||||
'course_settings': _attr_safe_json(course_settings)
|
||||
'course_settings': json.dumps(course_settings)
|
||||
}
|
||||
# print "start rendering.."
|
||||
return render_to_response('discussion/index.html', context)
|
||||
@@ -385,19 +377,19 @@ def single_thread(request, course_key, discussion_id, thread_id):
|
||||
'discussion_id': discussion_id,
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'init': '', # TODO: What is this?
|
||||
'user_info': _attr_safe_json(user_info),
|
||||
'can_create_comment': _attr_safe_json(
|
||||
'user_info': json.dumps(user_info),
|
||||
'can_create_comment': json.dumps(
|
||||
has_permission(request.user, "create_comment", course.id)),
|
||||
'can_create_subcomment': _attr_safe_json(
|
||||
'can_create_subcomment': json.dumps(
|
||||
has_permission(request.user, "create_sub_comment", course.id)),
|
||||
'can_create_thread': has_permission(request.user, "create_thread", course.id),
|
||||
'annotated_content_info': _attr_safe_json(annotated_content_info),
|
||||
'annotated_content_info': json.dumps(annotated_content_info),
|
||||
'course': course,
|
||||
#'recent_active_threads': recent_active_threads,
|
||||
'course_id': course.id.to_deprecated_string(), # TODO: Why pass both course and course.id to template?
|
||||
'thread_id': thread_id,
|
||||
'threads': _attr_safe_json(threads),
|
||||
'roles': _attr_safe_json(utils.get_role_ids(course_key)),
|
||||
'threads': json.dumps(threads),
|
||||
'roles': json.dumps(utils.get_role_ids(course_key)),
|
||||
'is_moderator': is_moderator,
|
||||
'thread_pages': query_params['num_pages'],
|
||||
'is_course_cohorted': is_course_cohorted(course_key),
|
||||
@@ -409,7 +401,7 @@ def single_thread(request, course_key, discussion_id, thread_id):
|
||||
'user_cohort': user_cohort,
|
||||
'sort_preference': cc_user.default_sort_key,
|
||||
'category_map': course_settings["category_map"],
|
||||
'course_settings': _attr_safe_json(course_settings)
|
||||
'course_settings': json.dumps(course_settings)
|
||||
}
|
||||
return render_to_response('discussion/index.html', context)
|
||||
|
||||
@@ -458,7 +450,7 @@ def user_profile(request, course_key, user_id):
|
||||
'discussion_data': threads,
|
||||
'page': query_params['page'],
|
||||
'num_pages': query_params['num_pages'],
|
||||
'annotated_content_info': _attr_safe_json(annotated_content_info),
|
||||
'annotated_content_info': json.dumps(annotated_content_info),
|
||||
})
|
||||
else:
|
||||
django_user = User.objects.get(id=user_id)
|
||||
@@ -467,9 +459,9 @@ def user_profile(request, course_key, user_id):
|
||||
'user': request.user,
|
||||
'django_user': django_user,
|
||||
'profiled_user': profiled_user.to_dict(),
|
||||
'threads': _attr_safe_json(threads),
|
||||
'user_info': _attr_safe_json(user_info),
|
||||
'annotated_content_info': _attr_safe_json(annotated_content_info),
|
||||
'threads': json.dumps(threads),
|
||||
'user_info': json.dumps(user_info, default=lambda x: None),
|
||||
'annotated_content_info': json.dumps(annotated_content_info),
|
||||
'page': query_params['page'],
|
||||
'num_pages': query_params['num_pages'],
|
||||
'learner_profile_page_url': reverse('learner_profile', kwargs={'username': django_user.username})
|
||||
@@ -546,9 +538,9 @@ def followed_threads(request, course_key, user_id):
|
||||
'user': request.user,
|
||||
'django_user': User.objects.get(id=user_id),
|
||||
'profiled_user': profiled_user.to_dict(),
|
||||
'threads': _attr_safe_json(threads),
|
||||
'user_info': _attr_safe_json(user_info),
|
||||
'annotated_content_info': _attr_safe_json(annotated_content_info),
|
||||
'threads': json.dumps(threads),
|
||||
'user_info': json.dumps(user_info),
|
||||
'annotated_content_info': json.dumps(annotated_content_info),
|
||||
# 'content': content,
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ class GroupIdAssertionMixin(object):
|
||||
|
||||
def _assert_html_response_contains_group_info(self, response):
|
||||
group_info = {"group_id": None, "group_name": None}
|
||||
match = re.search(r'"group_id": ([\d]*)', response.content)
|
||||
match = re.search(r'"group_id": ([\d]*)', response.content)
|
||||
if match and match.group(1) != '':
|
||||
group_info["group_id"] = int(match.group(1))
|
||||
match = re.search(r'"group_name": "([^&]*)"', response.content)
|
||||
match = re.search(r'"group_name": "([^&]*)"', response.content)
|
||||
if match:
|
||||
group_info["group_name"] = match.group(1)
|
||||
self._assert_thread_contains_group_info(group_info)
|
||||
|
||||
Reference in New Issue
Block a user