Commit ab78069a by Adam

Merge pull request #10246 from edx/merge-release-into-master

Merge release into master
parents 8f142339 9b3841f6
......@@ -681,6 +681,16 @@ class MiscCourseTests(ContentStoreTestCase):
for expected in expected_types:
self.assertIn(expected, resp.content)
@ddt.data("<script>alert(1)</script>", "alert('hi')", "</script><script>alert(1)</script>")
def test_container_handler_xss_prevent(self, malicious_code):
"""
Test that XSS attack is prevented
"""
resp = self.client.get_html(get_url('container_handler', self.vert_loc) + '?action=' + malicious_code)
self.assertEqual(resp.status_code, 200)
# Test that malicious code does not appear in html
self.assertNotIn(malicious_code, resp.content)
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', [])
def test_advanced_components_in_edit_unit(self):
# This could be made better, but for now let's just assert that we see the advanced modules mentioned in the page
......
......@@ -2,6 +2,7 @@
Public views
"""
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.clickjacking import xframe_options_deny
from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
......@@ -17,6 +18,7 @@ __all__ = ['signup', 'login_page', 'howitworks']
@ensure_csrf_cookie
@xframe_options_deny
def signup(request):
"""
Display the signup form.
......@@ -34,6 +36,7 @@ def signup(request):
@ssl_login_shortcut
@ensure_csrf_cookie
@xframe_options_deny
def login_page(request):
"""
Display the login form.
......
......@@ -13,28 +13,28 @@ import json
from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name
from django.utils.translation import ugettext as _
%>
<%block name="title">${xblock.display_name_with_default} ${xblock_type_display_name(xblock)}</%block>
<%block name="title">${xblock.display_name_with_default} ${xblock_type_display_name(xblock) | h}</%block>
<%block name="bodyclass">is-signedin course container view-container</%block>
<%namespace name='static' file='static_content.html'/>
<%block name="header_extras">
% for template_name in templates:
<script type="text/template" id="${template_name}-tpl">
<script type="text/template" id="${template_name | h}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
<script type="text/template" id="image-modal-tpl">
<%static:include path="common/templates/image-modal.underscore" />
</script>
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css') | h}" />
</%block>
<%block name="requirejs">
require(["js/factories/container"], function(ContainerFactory) {
ContainerFactory(
${component_templates | n}, ${json.dumps(xblock_info) | n},
"${action}",
"${action | h}",
{
isUnitPage: ${json.dumps(is_unit_page)},
canEdit: true
......@@ -55,7 +55,7 @@ from django.utils.translation import ugettext as _
ancestor_url = xblock_studio_url(ancestor)
%>
% if ancestor_url:
<a href="${ancestor_url}" class="navigation-item navigation-link navigation-parent">${ancestor.display_name_with_default | h}</a>
<a href="${ancestor_url | h}" class="navigation-item navigation-link navigation-parent">${ancestor.display_name_with_default | h}</a>
% else:
<span class="navigation-item navigation-parent">${ancestor.display_name_with_default | h}</span>
% endif
......@@ -72,12 +72,12 @@ from django.utils.translation import ugettext as _
<ul>
% if is_unit_page:
<li class="action-item action-view nav-item">
<a href="${published_preview_link}" class="button button-view action-button is-disabled" aria-disabled="true" rel="external" title="${_('Open the courseware in the LMS')}">
<a href="${published_preview_link | h}" class="button button-view action-button is-disabled" aria-disabled="true" rel="external" title="${_('Open the courseware in the LMS')}">
<span class="action-button-text">${_("View Live Version")}</span>
</a>
</li>
<li class="action-item action-preview nav-item">
<a href="${draft_preview_link}" class="button button-preview action-button" rel="external" title="${_('Preview the courseware in the LMS')}">
<a href="${draft_preview_link | h}" class="button button-preview action-button" rel="external" title="${_('Preview the courseware in the LMS')}">
<span class="action-button-text">${_("Preview")}</span>
</a>
</li>
......@@ -110,10 +110,10 @@ from django.utils.translation import ugettext as _
% if xblock.category == 'split_test':
<div class="bit">
<h3 class="title-3">${_("Adding components")}</h3>
<p>${_("Select a component type under {em_start}Add New Component{em_end}. Then select a template.").format(em_start='<strong>', em_end="</strong>")}</p>
<p>${_("Select a component type under {em_start}Add New Component{em_end}. Then select a template.").format(em_start='<strong>', em_end="</strong>") | h}</p>
<p>${_("The new component is added at the bottom of the page or group. You can then edit and move the component.")}</p>
<h3 class="title-3">${_("Editing components")}</h3>
<p>${_("Click the {em_start}Edit{em_end} icon in a component to edit its content.").format(em_start='<strong>', em_end="</strong>")}</p>
<p>${_("Click the {em_start}Edit{em_end} icon in a component to edit its content.").format(em_start='<strong>', em_end="</strong>") | h}</p>
<h3 class="title-3">${_("Reorganizing components")}</h3>
<p>${_("Drag components to new locations within this component.")}</p>
<p>${_("For content experiments, you can drag components to other groups.")}</p>
......@@ -121,7 +121,7 @@ from django.utils.translation import ugettext as _
<p>${_("Confirm that you have properly configured content in each of your experiment groups.")}</p>
</div>
<div class="bit external-help">
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about component containers")}</a>
<a href="${get_online_help_info(online_help_token())['doc_url'] | h}" target="_blank" class="button external-help-button">${_("Learn more about component containers")}</a>
</div>
% elif is_unit_page:
<div id="publish-unit"></div>
......
"""
Decorators that can be used to interact with third_party_auth.
"""
from functools import wraps
from urlparse import urlparse
from django.conf import settings
from django.utils.decorators import available_attrs
from third_party_auth.models import LTIProviderConfig
def xframe_allow_whitelisted(view_func):
"""
Modifies a view function so that its response has the X-Frame-Options HTTP header
set to 'DENY' if the request HTTP referrer is not from a whitelisted hostname.
"""
def wrapped_view(request, *args, **kwargs):
""" Modify the response with the correct X-Frame-Options. """
resp = view_func(request, *args, **kwargs)
x_frame_option = 'DENY'
if settings.FEATURES['ENABLE_THIRD_PARTY_AUTH']:
referer = request.META.get('HTTP_REFERER')
if referer is not None:
parsed_url = urlparse(referer)
hostname = parsed_url.hostname
if LTIProviderConfig.objects.current_set().filter(lti_hostname=hostname, enabled=True).exists():
x_frame_option = 'ALLOW'
resp['X-Frame-Options'] = x_frame_option
return resp
return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view)
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'LTIProviderConfig.lti_hostname'
db.add_column('third_party_auth_ltiproviderconfig', 'lti_hostname',
self.gf('django.db.models.fields.CharField')(default='localhost', max_length=255, db_index=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'LTIProviderConfig.lti_hostname'
db.delete_column('third_party_auth_ltiproviderconfig', 'lti_hostname')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'third_party_auth.ltiproviderconfig': {
'Meta': {'object_name': 'LTIProviderConfig'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lti_consumer_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'lti_consumer_secret': ('django.db.models.fields.CharField', [], {'default': "'ae8c9adcb7764ad67272c57c602dabd0b71acf22'", 'max_length': '255', 'blank': 'True'}),
'lti_hostname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'lti_max_timestamp_age': ('django.db.models.fields.IntegerField', [], {'default': '10'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'secondary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'skip_email_verification': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'skip_registration_form': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
},
'third_party_auth.oauth2providerconfig': {
'Meta': {'object_name': 'OAuth2ProviderConfig'},
'backend_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'key': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'other_settings': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'secondary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'secret': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'skip_email_verification': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'skip_registration_form': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
},
'third_party_auth.samlconfiguration': {
'Meta': {'object_name': 'SAMLConfiguration'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'entity_id': ('django.db.models.fields.CharField', [], {'default': "'http://saml.example.com'", 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org_info_str': ('django.db.models.fields.TextField', [], {'default': '\'{"en-US": {"url": "http://www.example.com", "displayname": "Example Inc.", "name": "example"}}\''}),
'other_config_str': ('django.db.models.fields.TextField', [], {'default': '\'{\\n"SECURITY_CONFIG": {"metadataCacheDuration": 604800, "signMetadata": false}\\n}\''}),
'private_key': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'public_key': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'third_party_auth.samlproviderconfig': {
'Meta': {'object_name': 'SAMLProviderConfig'},
'attr_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'attr_first_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'attr_full_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'attr_last_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'attr_user_permanent_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'attr_username': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'backend_name': ('django.db.models.fields.CharField', [], {'default': "'tpa-saml'", 'max_length': '50'}),
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'entity_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'idp_slug': ('django.db.models.fields.SlugField', [], {'max_length': '30'}),
'metadata_source': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'other_settings': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'secondary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'skip_email_verification': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'skip_registration_form': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
},
'third_party_auth.samlproviderdata': {
'Meta': {'ordering': "('-fetched_at',)", 'object_name': 'SAMLProviderData'},
'entity_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'expires_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'fetched_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'public_key': ('django.db.models.fields.TextField', [], {}),
'sso_url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
}
}
complete_apps = ['third_party_auth']
\ No newline at end of file
......@@ -493,6 +493,15 @@ class LTIProviderConfig(ProviderConfig):
'The name that the LTI Tool Consumer will use to identify itself'
)
)
lti_hostname = models.CharField(
max_length=255,
help_text=(
'The domain that will be acting as the LTI consumer.'
),
db_index=True
)
lti_consumer_secret = models.CharField(
default=long_token,
max_length=255,
......
"""
Tests for third_party_auth decorators.
"""
import ddt
import unittest
from django.conf import settings
from django.http import HttpResponse
from django.test import RequestFactory
from third_party_auth.decorators import xframe_allow_whitelisted
from third_party_auth.tests.testutil import TestCase
@xframe_allow_whitelisted
def mock_view(_request):
""" A test view for testing purposes. """
return HttpResponse()
# remove this decorator once third_party_auth is enabled in CMS
@unittest.skipIf(
'third_party_auth' not in settings.INSTALLED_APPS,
'third_party_auth is not currently installed in CMS'
)
@ddt.ddt
class TestXFrameWhitelistDecorator(TestCase):
""" Test the xframe_allow_whitelisted decorator. """
def setUp(self):
super(TestXFrameWhitelistDecorator, self).setUp()
self.configure_lti_provider(name='Test', lti_hostname='localhost', lti_consumer_key='test_key', enabled=True)
self.factory = RequestFactory()
def construct_request(self, referer):
""" Add the given referer to a request and then return it. """
request = self.factory.get('/login')
request.META['HTTP_REFERER'] = referer
return request
@ddt.unpack
@ddt.data(
('http://localhost:8000/login', 'ALLOW'),
('http://not-a-real-domain.com/login', 'DENY'),
(None, 'DENY')
)
def test_x_frame_options(self, url, expected_result):
request = self.construct_request(url)
response = mock_view(request)
self.assertEqual(response['X-Frame-Options'], expected_result)
@ddt.data('http://localhost/login', 'http://not-a-real-domain.com', None)
def test_feature_flag_off(self, url):
with self.settings(FEATURES={'ENABLE_THIRD_PARTY_AUTH': False}):
request = self.construct_request(url)
response = mock_view(request)
self.assertEqual(response['X-Frame-Options'], 'DENY')
......@@ -680,6 +680,16 @@ class ProgressPageTests(ModuleStoreTestCase):
self.section = ItemFactory.create(category='sequential', parent_location=self.chapter.location)
self.vertical = ItemFactory.create(category='vertical', parent_location=self.section.location)
@ddt.data('"><script>alert(1)</script>', '<script>alert(1)</script>', '</script><script>alert(1)</script>')
def test_progress_page_xss_prevent(self, malicious_code):
"""
Test that XSS attack is prevented
"""
resp = views.progress(self.request, course_id=unicode(self.course.id), student_id=self.user.id)
self.assertEqual(resp.status_code, 200)
# Test that malicious code does not appear in html
self.assertNotIn(malicious_code, resp.content)
def test_pure_ungraded_xblock(self):
ItemFactory.create(category='acid', parent_location=self.vertical.location)
......
......@@ -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'&quot;group_name&quot;: &quot;student_cohort&quot;')
self.assertRegexpMatches(html, r'&#34;group_name&#34;: &#34;student_cohort&#34;')
@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,56 @@ 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_user, mock_req): # pylint: disable=unused-argument
"""
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
......@@ -68,13 +67,6 @@ class DiscussionTab(EnrolledTab):
return utils.is_discussion_enabled(course.id)
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):
"""
......@@ -273,28 +265,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)
......@@ -380,19 +372,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),
......@@ -404,7 +396,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)
......@@ -453,7 +445,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)
......@@ -462,9 +454,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})
......@@ -541,9 +533,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'&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)
......
......@@ -354,6 +354,24 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
self.assertContains(resp, "Register for Test Microsite")
self.assertContains(resp, "register-form")
def test_login_registration_xframe_protected(self):
resp = self.client.get(
reverse("register_user"),
{},
HTTP_REFERER="http://localhost/iframe"
)
self.assertEqual(resp['X-Frame-Options'], 'DENY')
self.configure_lti_provider(name='Test', lti_hostname='localhost', lti_consumer_key='test_key', enabled=True)
resp = self.client.get(
reverse("register_user"),
HTTP_REFERER="http://localhost/iframe"
)
self.assertEqual(resp['X-Frame-Options'], 'ALLOW')
def _assert_third_party_auth_data(self, response, current_backend, current_provider, providers):
"""Verify that third party auth info is rendered correctly in a DOM data attribute. """
finish_auth_url = None
......
......@@ -34,6 +34,7 @@ from student.views import (
from student.helpers import get_next_url_for_login_page
import third_party_auth
from third_party_auth import pipeline
from third_party_auth.decorators import xframe_allow_whitelisted
from util.bad_request_rate_limiter import BadRequestRateLimiter
from openedx.core.djangoapps.user_api.accounts.api import request_password_change
......@@ -45,6 +46,7 @@ AUDIT_LOG = logging.getLogger("audit")
@require_http_methods(['GET'])
@ensure_csrf_cookie
@xframe_allow_whitelisted
def login_and_registration_form(request, initial_mode="login"):
"""Render the combined login/registration form, defaulting to login
......
......@@ -21,13 +21,13 @@ from django.utils.http import urlquote_plus
<%block name="nav_skip">#course-info-progress</%block>
<%block name="js_extra">
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script>
<script type="text/javascript" src="${static.url('js/courseware/certificates_api.js')}"></script>
<script type="text/javascript" src="${static.url('js/courseware/credit_progress.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js') | h}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js') | h}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js') | h}"></script>
<script type="text/javascript" src="${static.url('js/courseware/certificates_api.js') | h}"></script>
<script type="text/javascript" src="${static.url('js/courseware/credit_progress.js') | h}"></script>
<script>
${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph", not course.no_grade, not course.no_grade)}
${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph", not course.no_grade, not course.no_grade) | h}
</script>
</%block>
......@@ -40,12 +40,12 @@ from django.utils.http import urlquote_plus
<div class="course-info" id="course-info-progress" aria-label="${_('Course Progress')}">
% if staff_access and studio_url is not None:
<div class="wrap-instructor-info">
<a class="instructor-info-action studio-view" href="${studio_url}">${_("View Grading in studio")}</a>
<a class="instructor-info-action studio-view" href="${studio_url | h}">${_("View Grading in studio")}</a>
</div>
% endif
<header class="progress-certificates">
<h1 class="progress-certificates-title">${_("Course Progress for Student '{username}' ({email})").format(username=student.username, email=student.email)}</h1>
<h1 class="progress-certificates-title">${_("Course Progress for Student '{username}' ({email})").format(username=student.username, email=student.email) | h}</h1>
</header>
%if show_generate_cert_btn:
......@@ -64,11 +64,11 @@ from django.utils.http import urlquote_plus
</div>
<div class="msg-actions">
%if show_cert_web_view and cert_web_view_url:
<a class="btn" href="${cert_web_view_url}" target="_blank" title="${_('View certificate in a new browser window or tab.')}">
<a class="btn" href="${cert_web_view_url | h}" target="_blank" title="${_('View certificate in a new browser window or tab.')}">
${_("View Certificate")}
</a>
%elif download_url:
<a class="btn" href="${download_url}" target="_blank" title="${_('PDF will open in a new browser window or tab.')}">
<a class="btn" href="${download_url | h}" target="_blank" title="${_('PDF will open in a new browser window or tab.')}">
${_("Download Your Certificate")}
</a>
%endif
......@@ -86,7 +86,7 @@ from django.utils.http import urlquote_plus
<p class="copy">${_("You can keep working for a higher grade, or request your certificate now.")}</p>
</div>
<div class="msg-actions">
<button class="btn generate_certs" data-endpoint="${post_url}" id="btn_generate_cert">${_('Request Certificate')}</button>
<button class="btn generate_certs" data-endpoint="${post_url | h}" id="btn_generate_cert">${_('Request Certificate')}</button>
</div>
%endif
</div>
......@@ -106,25 +106,25 @@ from django.utils.http import urlquote_plus
<h2>${_("Requirements for Course Credit")}</h2>
</div>
%if credit_course_requirements['eligibility_status'] == 'not_eligible':
<span class="eligibility_msg">${_("{student_name}, you are no longer eligible for credit in this course.").format(student_name=student.profile.name)}</span>
<span class="eligibility_msg">${_("{student_name}, you are no longer eligible for credit in this course.").format(student_name=student.profile.name) | h}</span>
%elif credit_course_requirements['eligibility_status'] == 'eligible':
<span class="eligibility_msg">${_("{student_name}, you have met the requirements for credit in this course.").format(student_name=student.profile.name)}
<span class="eligibility_msg">${_("{student_name}, you have met the requirements for credit in this course.").format(student_name=student.profile.name) | h}
${_("{a_start}Go to your dashboard{a_end} to purchase course credit.").format(
a_start=u"<a href={url}>".format(url=reverse('dashboard')),
a_end="</a>"
)}
) | h}
</span>
%elif credit_course_requirements['eligibility_status'] == 'partial_eligible':
<span>${_("{student_name}, you have not yet met the requirements for credit.").format(student_name=student.profile.name)}</span>
<span>${_("{student_name}, you have not yet met the requirements for credit.").format(student_name=student.profile.name) | h}</span>
%endif
<a href="${settings.CREDIT_HELP_LINK_URL}" class="credit-help"><i class="fa fa-question"></i><span class="sr">${_("Information about course credit requirements")}</span></a><br>
<div class="requirement-container" data-eligible="${credit_course_requirements['eligibility_status']}">
<a href="${settings.CREDIT_HELP_LINK_URL | h}" class="credit-help"><i class="fa fa-question"></i><span class="sr">${_("Information about course credit requirements")}</span></a><br>
<div class="requirement-container" data-eligible="${credit_course_requirements['eligibility_status'] | h}">
%for requirement in credit_course_requirements['requirements']:
<div class="requirement">
<div class="requirement-name">
${_(requirement['display_name'])}
${_(requirement['display_name']) | h}
%if requirement['namespace'] == 'grade':
<span>${int(requirement['criteria']['min_grade'] * 100)}%</span>
<span>${int(requirement['criteria']['min_grade'] * 100) | h}%</span>
%endif
</div>
<div class="requirement-status">
......@@ -140,7 +140,7 @@ from django.utils.http import urlquote_plus
%elif requirement['status'] == 'satisfied':
<i class="fa fa-check" aria-hidden="true"></i>
% if requirement['namespace'] == 'grade' and 'final_grade' in requirement['reason']:
<span>${int(requirement['reason']['final_grade'] * 100)}%</span>
<span>${int(requirement['reason']['final_grade'] * 100) | h}%</span>
% else:
<span>${_("Completed")} ${get_time_display(requirement['status_date'], DEFAULT_SHORT_DATE_FORMAT, settings.TIME_ZONE)}</span>
% endif
......@@ -163,7 +163,7 @@ from django.utils.http import urlquote_plus
%for chapter in courseware_summary:
%if not chapter['display_name'] == "hidden":
<section>
<h2>${ chapter['display_name'] }</h2>
<h2>${ chapter['display_name'] | h}</h2>
<div class="sections">
%for section in chapter['sections']:
......@@ -174,20 +174,20 @@ from django.utils.http import urlquote_plus
percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 and total > 0 else ""
%>
<h3><a href="${reverse('courseware_section', kwargs=dict(course_id=course.id.to_deprecated_string(), chapter=chapter['url_name'], section=section['url_name']))}">
${ section['display_name'] }
<h3><a href="${reverse('courseware_section', kwargs=dict(course_id=course.id.to_deprecated_string(), chapter=chapter['url_name'], section=section['url_name'])) | h}">
${ section['display_name'] | h}
%if total > 0 or earned > 0:
<span class="sr">
${_("{earned} of {total} possible points").format(earned='{:.3n}'.format(float(earned)), total='{:.3n}'.format(float(total)))}
${_("{earned} of {total} possible points").format(earned='{:.3n}'.format(float(earned)), total='{:.3n}'.format(float(total))) | h}
</span>
%endif
</a>
%if total > 0 or earned > 0:
<span> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</span>
<span> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString ) | h}</span>
%endif
</h3>
<p>
${section['format']}
${section['format'] | h}
%if section.get('due') is not None:
<%
......@@ -195,7 +195,7 @@ from django.utils.http import urlquote_plus
due_date = '' if len(formatted_string)==0 else _(u'due {date}').format(date=formatted_string)
%>
<em>
${due_date}
${due_date | h}
</em>
%endif
</p>
......@@ -205,7 +205,7 @@ from django.utils.http import urlquote_plus
<h3> ${ _("Problem Scores: ") if section['graded'] else _("Practice Scores: ")} </h3>
<ol>
%for score in section['scores']:
<li>${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</li>
<li>${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible)) | h}</li>
%endfor
</ol>
%else:
......
......@@ -25,20 +25,20 @@ from django.core.urlresolvers import reverse
<%include file="_discussion_course_navigation.html" args="active_page='discussion'" />
<section class="discussion container" id="discussion-container"
data-roles="${roles}"
data-roles="${roles | h}"
data-course-id="${course_id | h}"
data-course-name="${course.display_name_with_default}"
data-user-info="${user_info}"
data-user-create-comment="${can_create_comment}"
data-user-create-subcomment="${can_create_subcomment}"
data-course-name="${course.display_name_with_default | h}"
data-user-info="${user_info | h}"
data-user-create-comment="${can_create_comment | h}"
data-user-create-subcomment="${can_create_subcomment | h}"
data-read-only="false"
data-threads="${threads}"
data-thread-pages="${thread_pages}"
data-content-info="${annotated_content_info}"
data-sort-preference="${sort_preference}"
data-flag-moderator="${flag_moderator}"
data-user-cohort-id="${user_cohort}"
data-course-settings="${course_settings}">
data-threads="${threads | h}"
data-thread-pages="${thread_pages | h}"
data-content-info="${annotated_content_info | h}"
data-sort-preference="${sort_preference | h}"
data-flag-moderator="${flag_moderator | h}"
data-user-cohort-id="${user_cohort | h}"
data-course-settings="${course_settings | h}">
<div class="discussion-body">
<div class="forum-nav" role="complementary" aria-label="${_("Discussion thread list")}"></div>
<div class="discussion-column" role="main" aria-label="Discussion" id="discussion-column">
......
......@@ -33,8 +33,7 @@ from django.template.defaultfilters import escapejs
</nav>
</section>
<section class="course-content container discussion-user-threads" data-course-id="${course.id | h}" data-course-name="${course.display_name_with_default}" data-threads="${threads}" data-user-info="${user_info}" data-page="${page}" data-num-pages="${num_pages}"/>
<section class="course-content container discussion-user-threads" data-course-id="${course.id | h}" data-course-name="${course.display_name_with_default | h}" data-threads="${threads | h}" data-user-info="${user_info | h}" data-page="${page | h}" data-num-pages="${num_pages | h}"/>
</div>
</section>
......
......@@ -2,7 +2,7 @@
# For Harvard courses:
-e git+https://github.com/gsehub/xblock-mentoring.git@4d1cce78dc232d5da6ffd73817b5c490e87a6eee#egg=xblock-mentoring
-e git+https://github.com/open-craft/problem-builder.git@859df4155c0031b5a70e7f7e9744b67b3ed331d7#egg=xblock-problem-builder
-e git+https://github.com/open-craft/problem-builder.git@1cb40ca523502ca2a8a2abe5aef4d1b6735cb5c7#egg=xblock-problem-builder
# Oppia XBlock
-e git+https://github.com/oppia/xblock.git@cd5479ee1138abfa278857d0113a45c2d05a983f#egg=oppia-xblock
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment