From 181f492ad021aeb43105aa9d38106ad7baf00211 Mon Sep 17 00:00:00 2001
From: Hiroki Kiyohara <hirokiky@gmail.com>
Date: Wed, 7 Dec 2016 03:06:58 +0900
Subject: [PATCH] Fixed #27416 -- Prevented ModelFormSet from creating objects
 for invalid PKs in data.

---
 django/forms/models.py        | 11 ++++++-----
 tests/model_formsets/tests.py | 22 ++++++++++++++++++++++
 2 files changed, 28 insertions(+), 5 deletions(-)

diff --git a/django/forms/models.py b/django/forms/models.py
index 3d74225434..a0f3ff53b3 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -755,12 +755,13 @@ class BaseModelFormSet(BaseFormSet):
         forms_to_delete = self.deleted_forms
         for form in self.initial_forms:
             obj = form.instance
+            # If the pk is None, it means either:
+            # 1. The object is an unexpected empty model, created by invalid
+            #    POST data such as an object outside the formset's queryset.
+            # 2. The object was already deleted from the database.
+            if obj.pk is None:
+                continue
             if form in forms_to_delete:
-                # If the pk is None, it means that the object can't be
-                # deleted again. Possible reason for this is that the
-                # object was already deleted from the DB. Refs #14877.
-                if obj.pk is None:
-                    continue
                 self.deleted_objects.append(obj)
                 self.delete_existing(obj, commit=commit)
             elif form.has_changed():
diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py
index fc0dff8304..4387fbf52d 100644
--- a/tests/model_formsets/tests.py
+++ b/tests/model_formsets/tests.py
@@ -1631,6 +1631,28 @@ class ModelFormsetTest(TestCase):
             ['Please correct the duplicate data for subtitle which must be unique for the month in posted.']
         )
 
+    def test_prevent_change_outer_model_and_create_invalid_data(self):
+        author = Author.objects.create(name='Charles')
+        other_author = Author.objects.create(name='Walt')
+        AuthorFormSet = modelformset_factory(Author, fields='__all__')
+        data = {
+            'form-TOTAL_FORMS': '2',
+            'form-INITIAL_FORMS': '2',
+            'form-MAX_NUM_FORMS': '',
+            'form-0-id': str(author.id),
+            'form-0-name': 'Charles',
+            'form-1-id': str(other_author.id),  # A model not in the formset's queryset.
+            'form-1-name': 'Changed name',
+        }
+        # This formset is only for Walt Whitman and shouldn't accept data for
+        # other_author.
+        formset = AuthorFormSet(data=data, queryset=Author.objects.filter(id__in=(author.id,)))
+        self.assertTrue(formset.is_valid())
+        formset.save()
+        # The name of other_author shouldn't be changed and new models aren't
+        # created.
+        self.assertQuerysetEqual(Author.objects.all(), ['<Author: Charles>', '<Author: Walt>'])
+
 
 class TestModelFormsetOverridesTroughFormMeta(TestCase):
     def test_modelformset_factory_widgets(self):