Security advisory GHSA-wqjj-hx84-v449: Django Vulnerable to MySQL Injection
Package: django (pypi), vulnerable version: 1.4.10, fixed in: 1.4.11
CWE: CWE-89
Files changed by the fix:
django/db/models/fields/__init__.py

Fix diff:
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index adb07cfa949b..c10e2b13d5de 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -1013,6 +1013,12 @@ def __init__(self, verbose_name=None, name=None, path='', match=None,
         kwargs['max_length'] = kwargs.get('max_length', 100)
         Field.__init__(self, verbose_name, name, **kwargs)
 
+    def get_prep_value(self, value):
+        value = super(FilePathField, self).get_prep_value(value)
+        if value is None:
+            return None
+        return six.text_type(value)
+
     def formfield(self, **kwargs):
         defaults = {
             'path': self.path,
@@ -1120,6 +1126,12 @@ def __init__(self, *args, **kwargs):
         kwargs['max_length'] = 15
         Field.__init__(self, *args, **kwargs)
 
+    def get_prep_value(self, value):
+        value = super(IPAddressField, self).get_prep_value(value)
+        if value is None:
+            return None
+        return six.text_type(value)
+
     def get_internal_type(self):
         return "IPAddressField"
 
@@ -1158,12 +1170,14 @@ def get_db_prep_value(self, value, connection, prepared=False):
         return value or None
 
     def get_prep_value(self, value):
+        if value is None:
+            return value
         if value and ':' in value:
             try:
                 return clean_ipv6_address(value, self.unpack_ipv4)
             except exceptions.ValidationError:
                 pass
-        return value
+        return six.text_type(value)
 
     def formfield(self, **kwargs):
         defaults = {
diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt
index 0d4b96ec026b..29c6f7dd3efe 100644
--- a/docs/howto/custom-model-fields.txt
+++ b/docs/howto/custom-model-fields.txt
@@ -501,6 +501,16 @@ For example::
             return ''.join([''.join(l) for l in (value.north,
                     value.east, value.south, value.west)])
 
+.. warning::
+
+    If your custom field uses the ``CHAR``, ``VARCHAR`` or ``TEXT``
+    types for MySQL, you must make sure that :meth:`.get_prep_value`
+    always returns a string type. MySQL performs flexible and unexpected
+    matching when a query is performed on these types and the provided
+    value is an integer, which can cause queries to include unexpected
+    objects in their results. This problem cannot occur if you always
+    return a string type from :meth:`.get_prep_value`.
+
 Converting query values to database values
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt
index 883417bea005..0822b05b24a5 100644
--- a/docs/ref/databases.txt
+++ b/docs/ref/databases.txt
@@ -504,6 +504,22 @@ MySQL does not support the ``NOWAIT`` option to the ``SELECT ... FOR UPDATE``
 statement. If ``select_for_update()`` is used with ``nowait=True`` then a
 ``DatabaseError`` will be raised.
 
+Automatic typecasting can cause unexpected results
+--------------------------------------------------
+
+When performing a query on a string type, but with an integer value, MySQL will
+coerce the types of all values in the table to an integer before performing the
+comparison. If your table contains the values ``'abc'``, ``'def'`` and you
+query for ``WHERE mycolumn=0``, both rows will match. Similarly, ``WHERE mycolumn=1``
+will match the value ``'abc1'``. Therefore, string type fields included in Django
+will always cast the value to a string before using it in a query.
+
+If you implement custom model fields that inherit from :class:`~django.db.models.Field`
+directly, are overriding :meth:`~django.db.models.Field.get_prep_value`, or use
+:meth:`extra() <django.db.models.query.QuerySet.extra>` or
+:meth:`raw() <django.db.models.Manager.raw>`, you should ensure that you
+perform the appropriate typecasting.
+
 .. _sqlite-notes:
 
 SQLite notes
diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt
index a7bbf4d29019..30b58a6343e2 100644
--- a/docs/ref/models/querysets.txt
+++ b/docs/ref/models/querysets.txt
@@ -1132,6 +1132,16 @@ of the arguments is required, but you should use at least one of them.
 
       Entry.objects.extra(where=['headline=%s'], params=['Lennon'])
 
+.. warning::
+
+    If you are performing queries on MySQL, note that MySQL's silent type coercion
+    may cause unexpected results when mixing types. If you query on a string
+    type column, but with an integer value, MySQL will coerce the types of all values
+    in the table to an integer before performing the comparison. For example, if your
+    table contains the values ``'abc'``, ``'def'`` and you query for ``WHERE mycolumn=0``,
+    both rows will match. To prevent this, perform the correct typecasting
+    before using the value in a query.
+
 defer
 ~~~~~
 
diff --git a/docs/topics/db/sql.txt b/docs/topics/db/sql.txt
index 0aee70a6dc13..823901946b98 100644
--- a/docs/topics/db/sql.txt
+++ b/docs/topics/db/sql.txt
@@ -66,6 +66,16 @@ options that make it very powerful.
     database, but does nothing to enforce that. If the query does not
     return rows, a (possibly cryptic) error will result.
 
+.. warning::
+
+    If you are performing queries on MySQL, note that MySQL's silent type coercion
+    may cause unexpected results when mixing types. If you query on a string
+    type column, but with an integer value, MySQL will coerce the types of all values
+    in the table to an integer before performing the comparison. For example, if your
+    table contains the values ``'abc'``, ``'def'`` and you query for ``WHERE mycolumn=0``,
+    both rows will match. To prevent this, perform the correct typecasting
+    before using the value in a query.
+
 Mapping query fields to model fields
 ------------------------------------
 
diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py
index 60ce13dfb6e6..704ae0b822e3 100644
--- a/tests/model_fields/tests.py
+++ b/tests/model_fields/tests.py
@@ -7,7 +7,14 @@
 from django import forms
 from django.core.exceptions import ValidationError
 from django.db import connection, models, IntegrityError
-from django.db.models.fields.files import FieldFile
+from django.db.models.fields import (
+    AutoField, BigIntegerField, BinaryField, BooleanField, CharField,
+    CommaSeparatedIntegerField, DateField, DateTimeField, DecimalField,
+    EmailField, FilePathField, FloatField, IntegerField, IPAddressField,
+    GenericIPAddressField, NullBooleanField, PositiveIntegerField,
+    PositiveSmallIntegerField, SlugField, SmallIntegerField, TextField,
+    TimeField, URLField)
+from django.db.models.fields.files import FileField, ImageField
 from django.utils import six
 from django.utils import unittest
 
@@ -494,6 +501,94 @@ def test_genericipaddressfield_formfield_protocol(self):
         self.assertRaises(ValidationError, form_field.clean, '127.0.0.1')
 
 
+class PrepValueTest(test.TestCase):
+    def test_AutoField(self):
+        self.assertIsInstance(AutoField(primary_key=True).get_prep_value(1), int)
+
+    @unittest.skipIf(six.PY3, "Python 3 has no `long` type.")
+    def test_BigIntegerField(self):
+        self.assertIsInstance(BigIntegerField().get_prep_value(long(9999999999999999999)), long)
+
+    def test_BinaryField(self):
+        self.assertIsInstance(BinaryField().get_prep_value(b''), bytes)
+
+    def test_BooleanField(self):
+        self.assertIsInstance(BooleanField().get_prep_value(True), bool)
+
+    def test_CharField(self):
+        self.assertIsInstance(CharField().get_prep_value(''), six.text_type)
+        self.assertIsInstance(CharField().get_prep_value(0), six.text_type)
+
+    def test_CommaSeparatedIntegerField(self):
+        self.assertIsInstance(CommaSeparatedIntegerField().get_prep_value('1,2'), six.text_type)
+        self.assertIsInstance(CommaSeparatedIntegerField().get_prep_value(0), six.text_type)
+
+    def test_DateField(self):
+        self.assertIsInstance(DateField().get_prep_value(datetime.date.today()), datetime.date)
+
+    def test_DateTimeField(self):
+        self.assertIsInstance(DateTimeField().get_prep_value(datetime.datetime.now()), datetime.datetime)
+
+    def test_DecimalField(self):
+        self.assertIsInstance(DecimalField().get_prep_value(Decimal('1.2')), Decimal)
+
+    def test_EmailField(self):
+        self.assertIsInstance(EmailField().get_prep_value('mailbox@domain.com'), six.text_type)
+
+    def test_FileField(self):
+        self.assertIsInstance(FileField().get_prep_value('filename.ext'), six.text_type)
+        self.assertIsInstance(FileField().get_prep_value(0), six.text_type)
+
+    def test_FilePathField(self):
+        self.assertIsInstance(FilePathField().get_prep_value('tests.py'), six.text_type)
+        self.assertIsInstance(FilePathField().get_prep_value(0), six.text_type)
+
+    def test_FloatField(self):
+        self.assertIsInstance(FloatField().get_prep_value(1.2), float)
+
+    def test_ImageField(self):
+        self.assertIsInstance(ImageField().get_prep_value('filename.ext'), six.text_type)
+
+    def test_IntegerField(self):
+        self.assertIsInstance(IntegerField().get_prep_value(1), int)
+
+    def test_IPAddressField(self):
+        self.assertIsInstance(IPAddressField().get_prep_value('127.0.0.1'), six.text_type)
+        self.assertIsInstance(IPAddressField().get_prep_value(0), six.text_type)
+
+    def test_GenericIPAddressField(self):
+        self.assertIsInstance(GenericIPAddressField().get_prep_value('127.0.0.1'), six.text_type)
+        self.assertIsInstance(GenericIPAddressField().get_prep_value(0), six.text_type)
+
+    def test_NullBooleanField(self):
+        self.assertIsInstance(NullBooleanField().get_prep_value(True), bool)
+
+    def test_PositiveIntegerField(self):
+        self.assertIsInstance(PositiveIntegerField().get_prep_value(1), int)
+
+    def test_PositiveSmallIntegerField(self):
+        self.assertIsInstance(PositiveSmallIntegerField().get_prep_value(1), int)
+
+    def test_SlugField(self):
+        self.assertIsInstance(SlugField().get_prep_value('slug'), six.text_type)
+        self.assertIsInstance(SlugField().get_prep_value(0), six.text_type)
+
+    def test_SmallIntegerField(self):
+        self.assertIsInstance(SmallIntegerField().get_prep_value(1), int)
+
+    def test_TextField(self):
+        self.assertIsInstance(TextField().get_prep_value('Abc'), six.text_type)
+        self.assertIsInstance(TextField().get_prep_value(0), six.text_type)
+
+    def test_TimeField(self):
+        self.assertIsInstance(
+            TimeField().get_prep_value(datetime.datetime.now().time()),
+            datetime.time)
+
+    def test_URLField(self):
+        self.assertIsInstance(URLField().get_prep_value('http://domain.com'), six.text_type)
+
+
 class CustomFieldTests(unittest.TestCase):
 
     def test_14786(self):