From 90d7b912b9c451dfdfb38f5f1f598af3b879257f Mon Sep 17 00:00:00 2001 From: Alejandro Zamora Date: Fri, 23 Jun 2017 12:06:08 -0300 Subject: [PATCH] Fixed #28201 -- Added ProhibitNullCharactersValidator and used it on CharField form field. --- django/core/validators.py | 24 ++++++++++++++ django/forms/fields.py | 1 + docs/ref/validators.txt | 24 ++++++++++++++ docs/releases/2.0.txt | 7 +++- .../forms_tests/field_tests/test_charfield.py | 6 ++++ tests/validators/tests.py | 32 ++++++++++++++++--- 6 files changed, 88 insertions(+), 6 deletions(-) diff --git a/django/core/validators.py b/django/core/validators.py index bfeaa2c7dc..c6d5e6e06a 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -503,3 +503,27 @@ def get_available_image_extensions(): validate_image_file_extension = FileExtensionValidator( allowed_extensions=get_available_image_extensions(), ) + + +@deconstructible +class ProhibitNullCharactersValidator: + """Validate that the string doesn't contain the null character.""" + message = _('Null characters are not allowed.') + code = 'null_characters_not_allowed' + + def __init__(self, message=None, code=None): + if message is not None: + self.message = message + if code is not None: + self.code = code + + def __call__(self, value): + if '\x00' in str(value): + raise ValidationError(self.message, code=self.code) + + def __eq__(self, other): + return ( + isinstance(other, self.__class__) and + self.message == other.message and + self.code == other.code + ) diff --git a/django/forms/fields.py b/django/forms/fields.py index 8494e316dd..40fdb24a10 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -217,6 +217,7 @@ class CharField(Field): self.validators.append(validators.MinLengthValidator(int(min_length))) if max_length is not None: self.validators.append(validators.MaxLengthValidator(int(max_length))) + self.validators.append(validators.ProhibitNullCharactersValidator()) def to_python(self, value): """Return a string.""" diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt index 0136fb45a3..f15fcfac2f 100644 --- a/docs/ref/validators.txt +++ b/docs/ref/validators.txt @@ -304,3 +304,27 @@ to, or in lieu of custom ``field.clean()`` methods. Uses Pillow to ensure that ``value.name`` (``value`` is a :class:`~django.core.files.File`) has `a valid image extension `_. + +``ProhibitNullCharactersValidator`` +----------------------------------- + +.. class:: ProhibitNullCharactersValidator(message=None, code=None) + + .. versionadded:: 2.0 + + Raises a :exc:`~django.core.exceptions.ValidationError` if ``str(value)`` + contains one or more nulls characters (``'\x00'``). + + :param message: If not ``None``, overrides :attr:`.message`. + :param code: If not ``None``, overrides :attr:`code`. + + .. attribute:: message + + The error message used by + :exc:`~django.core.exceptions.ValidationError` if validation fails. + Defaults to ``"Null characters are not allowed."``. + + .. attribute:: code + + The error code used by :exc:`~django.core.exceptions.ValidationError` + if validation fails. Defaults to ``"null_characters_not_allowed"``. diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt index 36aea3aefc..0b2074ebb9 100644 --- a/docs/releases/2.0.txt +++ b/docs/releases/2.0.txt @@ -318,7 +318,12 @@ URLs Validators ~~~~~~~~~~ -* ... +* The new :class:`.ProhibitNullCharactersValidator` disallows the null + character in the input of the :class:`~django.forms.CharField` form field + and its subclasses. Null character input was observed from vulnerability + scanning tools. Most databases silently discard null characters, but + psycopg2 2.7+ raises an exception when trying to save a null character to + a char/text field with PostgreSQL. .. _backwards-incompatible-2.0: diff --git a/tests/forms_tests/field_tests/test_charfield.py b/tests/forms_tests/field_tests/test_charfield.py index d51f9d8007..8f6a27f473 100644 --- a/tests/forms_tests/field_tests/test_charfield.py +++ b/tests/forms_tests/field_tests/test_charfield.py @@ -123,3 +123,9 @@ class CharFieldTest(FormFieldAssertionsMixin, SimpleTestCase): def test_charfield_disabled(self): f = CharField(disabled=True) self.assertWidgetRendersTo(f, '') + + def test_null_characters_prohibited(self): + f = CharField() + msg = 'Null characters are not allowed.' + with self.assertRaisesMessage(ValidationError, msg): + f.clean('\x00something') diff --git a/tests/validators/tests.py b/tests/validators/tests.py index bc8ee7fb1d..af93e2a5de 100644 --- a/tests/validators/tests.py +++ b/tests/validators/tests.py @@ -9,11 +9,11 @@ from django.core.files.base import ContentFile from django.core.validators import ( BaseValidator, DecimalValidator, EmailValidator, FileExtensionValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator, - MinValueValidator, RegexValidator, URLValidator, int_list_validator, - validate_comma_separated_integer_list, validate_email, - validate_image_file_extension, validate_integer, validate_ipv4_address, - validate_ipv6_address, validate_ipv46_address, validate_slug, - validate_unicode_slug, + MinValueValidator, ProhibitNullCharactersValidator, RegexValidator, + URLValidator, int_list_validator, validate_comma_separated_integer_list, + validate_email, validate_image_file_extension, validate_integer, + validate_ipv4_address, validate_ipv6_address, validate_ipv46_address, + validate_slug, validate_unicode_slug, ) from django.test import SimpleTestCase @@ -264,6 +264,10 @@ TEST_DATA = [ (validate_image_file_extension, ContentFile('contents', name='file.PNG'), None), (validate_image_file_extension, ContentFile('contents', name='file.txt'), ValidationError), (validate_image_file_extension, ContentFile('contents', name='file'), ValidationError), + + (ProhibitNullCharactersValidator(), '\x00something', ValidationError), + (ProhibitNullCharactersValidator(), 'something', None), + (ProhibitNullCharactersValidator(), None, None), ] @@ -488,3 +492,21 @@ class TestValidatorEquality(TestCase): FileExtensionValidator(['txt']), FileExtensionValidator(['txt'], message='custom error message') ) + + def test_prohibit_null_characters_validator_equality(self): + self.assertEqual( + ProhibitNullCharactersValidator(message='message', code='code'), + ProhibitNullCharactersValidator(message='message', code='code') + ) + self.assertEqual( + ProhibitNullCharactersValidator(), + ProhibitNullCharactersValidator() + ) + self.assertNotEqual( + ProhibitNullCharactersValidator(message='message1', code='code'), + ProhibitNullCharactersValidator(message='message2', code='code') + ) + self.assertNotEqual( + ProhibitNullCharactersValidator(message='message', code='code1'), + ProhibitNullCharactersValidator(message='message', code='code2') + )