diff --git a/django/forms/utils.py b/django/forms/utils.py index df75e94f81..16017ef418 100644 --- a/django/forms/utils.py +++ b/django/forms/utils.py @@ -5,7 +5,7 @@ import sys import warnings from django.conf import settings -from django.utils.html import format_html, format_html_join +from django.utils.html import format_html, format_html_join, escape from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -55,8 +55,8 @@ class ErrorDict(dict): def as_data(self): return {f: e.as_data() for f, e in self.items()} - def as_json(self): - errors = {f: json.loads(e.as_json()) for f, e in self.items()} + def as_json(self, escape_html=False): + errors = {f: json.loads(e.as_json(escape_html=escape_html)) for f, e in self.items()} return json.dumps(errors) def as_ul(self): @@ -86,11 +86,12 @@ class ErrorList(UserList, list): def as_data(self): return self.data - def as_json(self): + def as_json(self, escape_html=False): errors = [] for error in ValidationError(self.data).error_list: + message = list(error)[0] errors.append({ - 'message': list(error)[0], + 'message': escape(message) if escape_html else message, 'code': error.code or '', }) return json.dumps(errors) diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 5a9f6a1cdc..33fcf85511 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -142,7 +142,7 @@ and methods with an ``as_`` prefix could render them, but it had to be done the other way around in order not to break code that expects rendered error messages in ``Form.errors``. -.. method:: Form.errors.as_json() +.. method:: Form.errors.as_json(escape_html=False) .. versionadded:: 1.7 @@ -152,6 +152,17 @@ Returns the errors serialized as JSON. {"sender": [{"message": "Enter a valid email address.", "code": "invalid"}], "subject": [{"message": "This field is required.", "code": "required"}]} +By default, ``as_json()`` does not escape its output. If you are using it for +something like AJAX requests to a form view where the client interprets the +response and inserts errors into the page, you'll want to be sure to escape the +results on the client-side to avoid the possibility of a cross-site scripting +attack. It's trivial to do so using a JavaScript library like jQuery - simply +use ``$(el).text(errorText)`` rather than ``.html()``. + +If for some reason you don't want to use client-side escaping, you can also +set ``escape_html=True`` and error messages will be escaped so you can use them +directly in HTML. + .. method:: Form.add_error(field, error) .. versionadded:: 1.7 diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index f2a77cb089..f06a31f393 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -2071,6 +2071,33 @@ class FormsTestCase(TestCase): } self.assertEqual(errors, control) + def test_error_dict_as_json_escape_html(self): + """#21962 - adding html escape flag to ErrorDict""" + class MyForm(Form): + foo = CharField() + bar = CharField() + + def clean(self): + raise ValidationError('
Non-field error.
', + code='secret', + params={'a': 1, 'b': 2}) + + control = { + 'foo': [{'code': 'required', 'message': 'This field is required.'}], + 'bar': [{'code': 'required', 'message': 'This field is required.'}], + '__all__': [{'code': 'secret', 'message': 'Non-field error.
'}] + } + + form = MyForm({}) + self.assertFalse(form.is_valid()) + + errors = json.loads(form.errors.as_json()) + self.assertEqual(errors, control) + + errors = json.loads(form.errors.as_json(escape_html=True)) + control['__all__'][0]['message'] = '<p>Non-field error.</p>' + self.assertEqual(errors, control) + def test_error_list(self): e = ErrorList() e.append('Foo')