Promptice

Django Model Formsets: Add edit_only Mode to Prevent New Object Creation

django__django-14725 · cardboard

Django Model Formsets: Add edit_only Mode to Prevent New Object Creation

You are working in the django/django repository at commit 0af9a5fc7d765aa05ea784e2c3237675f3bb4b49 (Django 4.1 alpha).

Problem

modelformset_factory() and inlineformset_factory() do not support an edit_only parameter. This is a security concern: a view that intends to let users edit existing model instances can be tricked into creating new ones if an attacker crafts a POST payload with extra forms that have no primary key.

Even setting extra=0 is not a complete defence, because a browser or attacker can raise the INITIAL_FORMS management-form value to make the formset treat extra submitted forms as if they were existing objects whose primary key merely got wiped — which causes save() to treat them as new instances and call INSERT.

The missing feature is an edit_only=False keyword argument on both factory functions. When edit_only=True the formset's save() must silently skip creating any new objects (it should only call save_existing_objects()), regardless of what the submitted data contains.

Requirements / Interface

All changes must be made inside django/forms/models.py in the cloned django/django repository at the pinned commit.

1. `modelformset_factory()` — add an edit_only=False keyword parameter. The factory must set FormSet.edit_only = edit_only on the dynamically-created formset class.

2. `inlineformset_factory()` — add an edit_only=False keyword parameter. Relay it to the underlying modelformset_factory() call so the same class attribute is set.

3. `BaseModelFormSet.save()` — when self.edit_only is True, skip the save_new_objects() call entirely. Existing objects that are already in the queryset must still be saved (or deleted if marked for deletion), just as without edit_only.

4. `edit_only` with `extra` — when edit_only=True, the factory should also enforce extra=0 (raise ValueError if a caller explicitly passes extra > 0 together with edit_only=True), to prevent the INITIAL_FORMS tamper vector described above.

The public surface the grader checks is:

# modelformset_factory path
EditFormSet = modelformset_factory(MyModel, fields='__all__', edit_only=True)

# inlineformset_factory path
EditInlineFormSet = inlineformset_factory(Parent, Child, fields='__all__', edit_only=True)

# Saving an edit_only formset must not INSERT new rows
formset = EditFormSet(data=..., queryset=MyModel.objects.all())
assert formset.is_valid()
saved = formset.save()  # must not create objects beyond those in the queryset

# Attempting to create with data outside the queryset is silently skipped

Examples

Basic edit_only usage

from django.forms import modelformset_factory
from myapp.models import Author

AuthorEditFormSet = modelformset_factory(Author, fields=["name"], edit_only=True)

# Two existing authors in the DB; POST data contains a third form with no pk.
formset = AuthorEditFormSet(
    data={
        "form-TOTAL_FORMS": "3",
        "form-INITIAL_FORMS": "2",
        "form-MIN_NUM_FORMS": "0",
        "form-MAX_NUM_FORMS": "1000",
        "form-0-id": "1",
        "form-0-name": "Alice",
        "form-1-id": "2",
        "form-1-name": "Bob",
        "form-2-id": "",
        "form-2-name": "Mallory",  # attacker's new record
    },
    queryset=Author.objects.filter(pk__in=[1, 2]),
)
assert formset.is_valid()
saved = formset.save()
# Only Alice and Bob are updated; Mallory is NOT created.
assert Author.objects.count() == 2

Object outside queryset is ignored

When a POST payload references a primary key that is not in the formset's queryset, edit_only=True must not create a new object for that pk either.

inlineformset_factory

from django.forms import inlineformset_factory
from myapp.models import Author, Book

BookInlineFormSet = inlineformset_factory(Author, Book, fields=["title"], edit_only=True)
# edit_only is forwarded; extra defaults to 0; new books are never INSERTed.

extra + edit_only conflict

# This should raise ValueError because extra > 0 is incompatible with edit_only.
modelformset_factory(Author, fields=["name"], edit_only=True, extra=1)

Constraints

  • All changes must be confined to django/forms/models.py. Do not modify the test files or any other module.
  • Do not alter the behaviour of formsets where edit_only=False (the default).
  • save_existing_objects() and deletion logic must continue to work normally.
  • The fix must not break any of the existing model_formsets tests.

Scoring

Your submission is accepted if:

  • The three hidden test_edit_only* tests pass.
  • All 57 listed pass-to-pass model_formsets regression tests continue to pass.

Leaderboards additionally rank accepted runs by tokens consumed, estimated cost, and wall-clock time.

Container

not started

Visible tests

60

Hidden tests

0

Last run

Not run

60 total0 passed0 failed
1

Test 1

fail to pass

edit only (model formsets.tests.ModelFormsetTest)

2

Test 2

fail to pass

edit only inlineformset factory (model formsets.tests.ModelFormsetTest)

3

Test 3

fail to pass

edit only object outside of queryset (model formsets.tests.ModelFormsetTest)

4

Test 4

pass to pass

deletion (model formsets.tests.DeletionTests)

5

Test 5

pass to pass

outdated deletion (model formsets.tests.DeletionTests)

6

Test 6

pass to pass

callable defaults (model formsets.tests.ModelFormsetTest)

7

Test 7

pass to pass

commit false (model formsets.tests.ModelFormsetTest)

8

Test 8

pass to pass

custom pk (model formsets.tests.ModelFormsetTest)

9

Test 9

pass to pass

custom save method (model formsets.tests.ModelFormsetTest)

10

Test 10

pass to pass

foreign keys in parents (model formsets.tests.ModelFormsetTest)

11

Test 11

pass to pass

initial form count empty data (model formsets.tests.ModelFormsetTest)

12

Test 12

pass to pass

inline formsets (model formsets.tests.ModelFormsetTest)

13

Test 13

pass to pass

inline formsets save as new (model formsets.tests.ModelFormsetTest)

14

Test 14

pass to pass

inline formsets with custom pk (model formsets.tests.ModelFormsetTest)

15

Test 15

pass to pass

inline formsets with custom save method (model formsets.tests.ModelFormsetTest)

16

Test 16

pass to pass

inline formsets with multi table inheritance (model formsets.tests.ModelFormsetTest)

17

Test 17

pass to pass

inline formsets with nullable unique together (model formsets.tests.ModelFormsetTest)

18

Test 18

pass to pass

inlineformset factory with null fk (model formsets.tests.ModelFormsetTest)

19

Test 19

pass to pass

inlineformset with arrayfield (model formsets.tests.ModelFormsetTest)

20

Test 20

pass to pass

max num (model formsets.tests.ModelFormsetTest)

21

Test 21

pass to pass

min num (model formsets.tests.ModelFormsetTest)

22

Test 22

pass to pass

min num with existing (model formsets.tests.ModelFormsetTest)

23

Test 23

pass to pass

model formset with custom pk (model formsets.tests.ModelFormsetTest)

24

Test 24

pass to pass

model formset with initial model instance (model formsets.tests.ModelFormsetTest)

25

Test 25

pass to pass

model formset with initial queryset (model formsets.tests.ModelFormsetTest)

26

Test 26

pass to pass

model inheritance (model formsets.tests.ModelFormsetTest)

27

Test 27

pass to pass

modelformset min num equals max num less than (model formsets.tests.ModelFormsetTest)

28

Test 28

pass to pass

modelformset min num equals max num more than (model formsets.tests.ModelFormsetTest)

29

Test 29

pass to pass

modelformset validate max flag (model formsets.tests.ModelFormsetTest)

30

Test 30

pass to pass

prevent change outer model and create invalid data (model formsets.tests.ModelFormsetTest)

31

Test 31

pass to pass

prevent duplicates from with the same formset (model formsets.tests.ModelFormsetTest)

32

Test 32

pass to pass

simple save (model formsets.tests.ModelFormsetTest)

33

Test 33

pass to pass

unique together validation (model formsets.tests.ModelFormsetTest)

34

Test 34

pass to pass

unique together with inlineformset factory (model formsets.tests.ModelFormsetTest)

35

Test 35

pass to pass

unique true enforces max num one (model formsets.tests.ModelFormsetTest)

36

Test 36

pass to pass

unique validation (model formsets.tests.ModelFormsetTest)

37

Test 37

pass to pass

validation with child model without id (model formsets.tests.ModelFormsetTest)

38

Test 38

pass to pass

validation with invalid id (model formsets.tests.ModelFormsetTest)

39

Test 39

pass to pass

validation with nonexistent id (model formsets.tests.ModelFormsetTest)

40

Test 40

pass to pass

validation without id (model formsets.tests.ModelFormsetTest)

41

Test 41

pass to pass

inlineformset factory absolute max (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

42

Test 42

pass to pass

inlineformset factory absolute max with max num (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

43

Test 43

pass to pass

inlineformset factory can delete extra (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

44

Test 44

pass to pass

inlineformset factory can not delete extra (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

45

Test 45

pass to pass

inlineformset factory error messages overrides (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

46

Test 46

pass to pass

inlineformset factory field class overrides (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

47

Test 47

pass to pass

inlineformset factory help text overrides (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

48

Test 48

pass to pass

inlineformset factory labels overrides (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

49

Test 49

pass to pass

inlineformset factory passes renderer (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

50

Test 50

pass to pass

inlineformset factory widgets (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

51

Test 51

pass to pass

modelformset factory absolute max (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

52

Test 52

pass to pass

modelformset factory absolute max with max num (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

53

Test 53

pass to pass

modelformset factory can delete extra (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

54

Test 54

pass to pass

modelformset factory disable delete extra (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

55

Test 55

pass to pass

modelformset factory error messages overrides (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

56

Test 56

pass to pass

modelformset factory field class overrides (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

57

Test 57

pass to pass

modelformset factory help text overrides (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

58

Test 58

pass to pass

modelformset factory labels overrides (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

59

Test 59

pass to pass

modelformset factory passes renderer (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

60

Test 60

pass to pass

modelformset factory widgets (model formsets.tests.TestModelFormsetOverridesTroughFormMeta)

README.md

django/django

Loading repository...code-server
Loading...
Workspace Terminal