Add template escaping in forums, progress page and course container TNL-2395, TNL-2394, TNL-2393

This commit is contained in:
mushtaqali
2015-06-12 16:51:48 +05:00
committed by Adam Palay
parent 0248f8af6d
commit 296f75a07c
9 changed files with 151 additions and 89 deletions

View File

@@ -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'&quot;id&quot;: &quot;{}&quot;'.format(self.TEST_THREAD_ID))
self.assertRegexpMatches(html, r'&quot;title&quot;: &quot;{}&quot;'.format(self.TEST_THREAD_TEXT))
self.assertRegexpMatches(html, r'&quot;body&quot;: &quot;{}&quot;'.format(self.TEST_THREAD_TEXT))
self.assertRegexpMatches(html, r'&quot;username&quot;: &quot;{}&quot;'.format(self.student.username))
self.assertRegexpMatches(html, r'&#34;id&#34;: &#34;{}&#34;'.format(self.TEST_THREAD_ID))
self.assertRegexpMatches(html, r'&#34;title&#34;: &#34;{}&#34;'.format(self.TEST_THREAD_TEXT))
self.assertRegexpMatches(html, r'&#34;body&#34;: &#34;{}&#34;'.format(self.TEST_THREAD_TEXT))
self.assertRegexpMatches(html, r'&#34;username&#34;: &#34;{}&#34;'.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()

View File

@@ -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), {'"': '&quot;'})
@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,
}

View File

@@ -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'&quot;group_id&quot;: ([\d]*)', response.content)
match = re.search(r'&#34;group_id&#34;: ([\d]*)', response.content)
if match and match.group(1) != '':
group_info["group_id"] = int(match.group(1))
match = re.search(r'&quot;group_name&quot;: &quot;([^&]*)&quot;', response.content)
match = re.search(r'&#34;group_name&#34;: &#34;([^&]*)&#34;', response.content)
if match:
group_info["group_name"] = match.group(1)
self._assert_thread_contains_group_info(group_info)