From abfe1a1c57a57cfaf6dd4a0571c029401a0fe743 Mon Sep 17 00:00:00 2001
From: Jacob Walls <jacobtylerwalls@gmail.com>
Date: Mon, 16 Mar 2026 18:05:22 -0400
Subject: [PATCH] [4.2.x] Fixed CVE-2026-4292 -- Disallowed instance creation
 via ModelAdmin.list_editable.

Thanks Natalia Bidart, Jake Howard, and Markus Holtermann for reviews.

Backport of 6afe7ce93964f56e33a29d477c269436f9b60cbf from main.
---
 django/contrib/admin/options.py |  3 +++
 docs/releases/4.2.30.txt        | 10 ++++++++++
 tests/admin_views/admin.py      | 10 +++++++++-
 tests/admin_views/tests.py      | 17 +++++++++++++++++
 4 files changed, 39 insertions(+), 1 deletion(-)

diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 1bbb302611a9..bb51884a8350 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -30,6 +30,7 @@
 from django.contrib.admin.widgets import AutocompleteSelect, AutocompleteSelectMultiple
 from django.contrib.auth import get_permission_codename
 from django.core.exceptions import (
+    BadRequest,
     FieldDoesNotExist,
     FieldError,
     PermissionDenied,
@@ -2016,6 +2017,8 @@ def changelist_view(self, request, extra_context=None):
                     for form in formset.forms:
                         if form.has_changed():
                             obj = self.save_form(request, form, change=True)
+                            if obj._state.adding:
+                                raise BadRequest("list_editable does not allow adding.")
                             self.save_model(request, obj, form, change=True)
                             self.save_related(request, form, formsets=[], change=True)
                             change_msg = self.construct_change_message(
diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
index 9241034ffb30..690de8d45cbe 100644
--- a/tests/admin_views/admin.py
+++ b/tests/admin_views/admin.py
@@ -358,6 +358,14 @@ def get_queryset(self, request):
         return super().get_queryset(request).order_by("age")
 
 
+class ParentWithUUIDPKAdmin(admin.ModelAdmin):
+    list_display = ("id", "title")
+    list_editable = ("title",)
+
+    def has_add_permission(self, request):
+        return False
+
+
 class FooAccountAdmin(admin.StackedInline):
     model = FooAccount
     extra = 1
@@ -1244,7 +1252,7 @@ class TravelerAdmin(admin.ModelAdmin):
 site.register(InlineReferer, InlineRefererAdmin)
 site.register(ReferencedByGenRel)
 site.register(GenRelReference)
-site.register(ParentWithUUIDPK)
+site.register(ParentWithUUIDPK, ParentWithUUIDPKAdmin)
 site.register(RelatedPrepopulated, search_fields=["name"])
 site.register(RelatedWithUUIDPKModel)
 site.register(ReadOnlyRelatedField, ReadOnlyRelatedFieldAdmin)
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index f40415681a0d..ddc725495815 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -2,6 +2,7 @@
 import os
 import re
 import unittest
+from http import HTTPStatus
 from unittest import mock
 from urllib.parse import parse_qsl, urljoin, urlparse
 
@@ -4100,6 +4101,22 @@ def test_post_submission(self):
 
         self.assertIs(Person.objects.get(name="John Mauchly").alive, False)
 
+    def test_forged_post_submission_when_no_add_permission(self):
+        before_count = ParentWithUUIDPK.objects.count()
+        data = {
+            "form-TOTAL_FORMS": "1",
+            "form-INITIAL_FORMS": "0",
+            "form-MAX_NUM_FORMS": "0",
+            "form-0-title": "The News",
+            "form-0-id": "",
+            "_save": "Save",
+        }
+        # This model admin allows no add permissions.
+        changelist_url = reverse("admin:admin_views_parentwithuuidpk_changelist")
+        response = self.client.post(changelist_url, data)
+        self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
+        self.assertEqual(ParentWithUUIDPK.objects.count(), before_count)
+
     def test_non_field_errors(self):
         """
         Non-field errors are displayed for each of the forms in the
