Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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,
Expand Down Expand Up @@ -2119,6 +2120,8 @@ def changelist_view(self, request, extra_context=None):
for form in cl.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(
Expand Down
15 changes: 15 additions & 0 deletions django/contrib/contenttypes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,21 @@ def get_formset(self, request, obj=None, **kwargs):
**kwargs,
}

base_model_form = defaults["form"]
can_change = self.has_change_permission(request, obj) if request else True
can_add = self.has_add_permission(request, obj) if request else True

class PermissionProtectedModelForm(base_model_form):
def has_changed(self):
# Protect against unauthorized edits.
if not can_change and not self.instance._state.adding:
return False
if not can_add and self.instance._state.adding:
return False
return super().has_changed()

defaults["form"] = PermissionProtectedModelForm

if defaults["fields"] is None and not modelform_defines_fields(
defaults["form"]
):
Expand Down
3 changes: 3 additions & 0 deletions django/core/handlers/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ def __init__(self, scope, body_file):
_headers = defaultdict(list)
for name, value in self.scope.get("headers", []):
name = name.decode("latin1")
# Prevent spoofing via ambiguity between underscores and hyphens.
if "_" in name:
continue
if name == "content-length":
corrected_name = "CONTENT_LENGTH"
elif name == "content-type":
Expand Down
15 changes: 9 additions & 6 deletions django/http/multipartparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,15 +305,18 @@ def _parse(self):
# We should always decode base64 chunks by
# multiple of 4, ignoring whitespace.

stripped_chunk = b"".join(chunk.split())
stripped_parts = [b"".join(chunk.split())]
stripped_length = len(stripped_parts[0])

remaining = len(stripped_chunk) % 4
while remaining != 0:
over_chunk = field_stream.read(4 - remaining)
while stripped_length % 4 != 0:
over_chunk = field_stream.read(self._chunk_size)
if not over_chunk:
break
stripped_chunk += b"".join(over_chunk.split())
remaining = len(stripped_chunk) % 4
over_stripped = b"".join(over_chunk.split())
stripped_parts.append(over_stripped)
stripped_length += len(over_stripped)

stripped_chunk = b"".join(stripped_parts)

try:
chunk = base64.b64decode(stripped_chunk)
Expand Down
30 changes: 21 additions & 9 deletions django/http/request.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import codecs
import copy
import operator
import os
from io import BytesIO
from itertools import chain
from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlsplit
Expand Down Expand Up @@ -401,15 +402,18 @@ def body(self):
)

# Limit the maximum request data size that will be handled
# in-memory.
if (
settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None
and int(self.META.get("CONTENT_LENGTH") or 0)
> settings.DATA_UPLOAD_MAX_MEMORY_SIZE
):
raise RequestDataTooBig(
"Request body exceeded settings.DATA_UPLOAD_MAX_MEMORY_SIZE."
)
# in-memory. Reject early when Content-Length is present and
# already exceeds the limit, avoiding reading the body at all.
self._check_data_too_big(int(self.META.get("CONTENT_LENGTH") or 0))

# Content-Length can be absent or understated (e.g.
# `Transfer-Encoding: chunked` on ASGI), so for seekable
# streams (e.g. SpooledTemporaryFile on ASGI), check the actual
# buffered size before reading it all into memory.
if self._stream.seekable():
stream_size = self._stream.seek(0, os.SEEK_END)
self._check_data_too_big(stream_size)
self._stream.seek(0)

try:
self._body = self.read()
Expand All @@ -420,6 +424,14 @@ def body(self):
self._stream = BytesIO(self._body)
return self._body

def _check_data_too_big(self, length):
if (
settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None
and length > settings.DATA_UPLOAD_MAX_MEMORY_SIZE
):
msg = "Request body exceeded settings.DATA_UPLOAD_MAX_MEMORY_SIZE."
raise RequestDataTooBig(msg)

def _mark_post_parse_error(self):
self._post = QueryDict()
self._files = MultiValueDict()
Expand Down
19 changes: 13 additions & 6 deletions django/tasks/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ class TaskResultStatus(TextChoices):

@dataclass(frozen=True, slots=True, kw_only=True)
class Task:
priority: int
func: Callable[..., Any] # The Task function.
backend: str
queue_name: str
run_after: datetime | None # The earliest this Task will run.
priority: int = DEFAULT_TASK_PRIORITY
backend: str = DEFAULT_TASK_BACKEND_ALIAS
queue_name: str = DEFAULT_TASK_QUEUE_NAME
run_after: datetime | None = None # The earliest this Task will run.

# Whether the Task receives the Task context when executed.
takes_context: bool = False
Expand Down Expand Up @@ -138,17 +138,24 @@ def task(
queue_name=DEFAULT_TASK_QUEUE_NAME,
backend=DEFAULT_TASK_BACKEND_ALIAS,
takes_context=False,
**kwargs,
):
from . import task_backends

if "run_after" in kwargs:
raise TypeError(
"run_after cannot be defined statically with the @task decorator. "
"Use .using(run_after=...) to set it dynamically."
)

def wrapper(f):
return task_backends[backend].task_class(
priority=priority,
func=f,
priority=priority,
queue_name=queue_name,
backend=backend,
takes_context=takes_context,
run_after=None,
**kwargs,
)

if function:
Expand Down
5 changes: 4 additions & 1 deletion django/test/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,10 @@ def generic(
if headers:
extra.update(HttpHeaders.to_asgi_names(headers))
s["headers"] += [
(key.lower().encode("ascii"), value.encode("latin1"))
# Avoid breaking test clients that just want to supply normalized
# ASGI names, regardless of the fact that ASGIRequest drops headers
# with underscores (CVE-2026-3902).
(key.lower().replace("_", "-").encode("ascii"), value.encode("latin1"))
for key, value in extra.items()
]
return self.request(**s)
Expand Down
21 changes: 13 additions & 8 deletions docs/ref/settings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1037,14 +1037,19 @@ The maximum size in bytes that a request body may be before a
:exc:`~django.core.exceptions.SuspiciousOperation` (``RequestDataTooBig``) is
raised. The check is done when accessing ``request.body`` or ``request.POST``
and is calculated against the total request size excluding any file upload
data. You can set this to ``None`` to disable the check. Applications that are
expected to receive unusually large form posts should tune this setting.

The amount of request data is correlated to the amount of memory needed to
process the request and populate the GET and POST dictionaries. Large requests
could be used as a denial-of-service attack vector if left unchecked. Since web
servers don't typically perform deep request inspection, it's not possible to
perform a similar check at that level.
data (``request.FILES``). You can set this to ``None`` to disable the check.
Applications that are expected to receive unusually large form posts should
tune this setting.

Under ASGI, the entire request may be spooled to disk before this limit is
enforced. Therefore, it is strongly recommended to place additional protections
in front of Django which limit the entire request payload.

The amount of request data is correlated to the amount of memory or storage
needed to process the request and populate the GET and POST dictionaries.
Large requests could be used as a denial-of-service attack vector if left
unchecked. Since web servers don't typically perform deep request inspection,
it's not possible to perform a similar check at that level.

See also :setting:`FILE_UPLOAD_MAX_MEMORY_SIZE`.

Expand Down
30 changes: 27 additions & 3 deletions docs/ref/tasks.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ Task definition
The ``task`` decorator
----------------------

.. function:: task(*, priority=0, queue_name="default", backend="default", takes_context=False)
.. function:: task(*, priority=0, queue_name="default", backend="default", takes_context=False, **kwargs)

The ``@task`` decorator defines a :class:`Task` instance. This has the
following optional arguments:
The ``@task`` decorator defines a :class:`Task` instance. All keyword
arguments are passed directly to the backend's ``task_class`` (which
defaults to :class:`Task`).

The following standard arguments are supported:

* ``priority``: Sets the :attr:`~Task.priority` of the ``Task``. Defaults
to 0.
Expand All @@ -32,6 +35,18 @@ The ``task`` decorator
:class:`TaskContext`. Defaults to ``False``. See :ref:`Task context
<task-context>` for details.

Custom Task backends may define a custom ``task_class`` that accepts
additional arguments. These can be passed through the ``@task`` decorator::

@task(foo=5, bar=600)
def my_task():
pass

.. versionchanged:: 6.1

Support for passing arbitrary ``**kwargs`` to the ``@task`` decorator
is added.

If the defined ``Task`` is not valid according to the backend,
:exc:`~django.tasks.exceptions.InvalidTask` is raised.

Expand Down Expand Up @@ -75,6 +90,8 @@ The ``task`` decorator
current time, a timezone-aware :class:`datetime <datetime.datetime>`,
or ``None`` if not constrained. Defaults to ``None``.

This attribute can be set using :meth:`~Task.using`.

The backend must have :attr:`.supports_defer` set to ``True`` to use
this feature. Otherwise,
:exc:`~django.tasks.exceptions.InvalidTask` is raised.
Expand Down Expand Up @@ -291,6 +308,13 @@ Base backend

``BaseTaskBackend`` is the parent class for all Task backends.

.. attribute:: BaseTaskBackend.task_class

The :class:`~django.tasks.Task` subclass to use when creating tasks
with the :func:`~django.tasks.task` decorator. Defaults to
:class:`~django.tasks.Task`. Custom backends can override this to use
a custom ``Task`` subclass with additional attributes.

.. attribute:: BaseTaskBackend.options

A dictionary of extra parameters for the Task backend. These are
Expand Down
61 changes: 61 additions & 0 deletions docs/releases/4.2.30.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,64 @@ Django 4.2.30 release notes

Django 4.2.30 fixes one security issue with severity "moderate" and four
security issues with severity "low" in 4.2.29.

CVE-2026-3902: ASGI header spoofing via underscore/hyphen conflation
====================================================================

``ASGIRequest`` normalizes header names following WSGI conventions, mapping
hyphens to underscores. As a result, even in configurations where reverse
proxies carefully strip security-sensitive headers named with hyphens, such a
header could be spoofed by supplying a header named with underscores.

Under WSGI, it is the responsibility of the server or proxy to avoid ambiguous
mappings. (Django's :djadmin:`runserver` was patched in :cve:`2015-0219`.) But
under ASGI, there is not the same uniform expectation, even if many proxies
protect against this under default configuration (including ``nginx`` via
``underscores_in_headers off;``).

Headers containing underscores are now ignored by ``ASGIRequest``, matching the
behavior of :pypi:`Daphne <daphne>`, the reference server for ASGI.

This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.

CVE-2026-4277: Privilege abuse in ``GenericInlineModelAdmin``
=============================================================

Add permissions on inline model instances were not validated on submission of
forged ``POST`` data in
:class:`~django.contrib.contenttypes.admin.GenericInlineModelAdmin`.

This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.

CVE-2026-4292: Privilege abuse in ``ModelAdmin.list_editable``
==============================================================

Admin changelist forms using
:attr:`~django.contrib.admin.ModelAdmin.list_editable` incorrectly allowed new
instances to be created via forged ``POST`` data.

This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.

CVE-2026-33033: Potential denial-of-service vulnerability in ``MultiPartParser`` via base64-encoded file upload
===============================================================================================================

When using ``django.http.multipartparser.MultiPartParser``, multipart uploads
with ``Content-Transfer-Encoding: base64`` that include excessive whitespace
may trigger repeated memory copying, potentially degrading performance.

This issue has severity "moderate" according to the :ref:`Django security
policy <security-disclosure>`.

CVE-2026-33034: Potential denial-of-service vulnerability in ASGI requests via memory upload limit bypass
=========================================================================================================

ASGI requests with a missing or understated ``Content-Length`` header could
bypass the :setting:`DATA_UPLOAD_MAX_MEMORY_SIZE` limit when reading
``HttpRequest.body``, potentially loading an unbounded request body into
memory and causing service degradation.

This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.
61 changes: 61 additions & 0 deletions docs/releases/5.2.13.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,64 @@ Django 5.2.13 release notes

Django 5.2.13 fixes one security issue with severity "moderate" and four
security issues with severity "low" in 5.2.12.

CVE-2026-3902: ASGI header spoofing via underscore/hyphen conflation
====================================================================

``ASGIRequest`` normalizes header names following WSGI conventions, mapping
hyphens to underscores. As a result, even in configurations where reverse
proxies carefully strip security-sensitive headers named with hyphens, such a
header could be spoofed by supplying a header named with underscores.

Under WSGI, it is the responsibility of the server or proxy to avoid ambiguous
mappings. (Django's :djadmin:`runserver` was patched in :cve:`2015-0219`.) But
under ASGI, there is not the same uniform expectation, even if many proxies
protect against this under default configuration (including ``nginx`` via
``underscores_in_headers off;``).

Headers containing underscores are now ignored by ``ASGIRequest``, matching the
behavior of :pypi:`Daphne <daphne>`, the reference server for ASGI.

This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.

CVE-2026-4277: Privilege abuse in ``GenericInlineModelAdmin``
=============================================================

Add permissions on inline model instances were not validated on submission of
forged ``POST`` data in
:class:`~django.contrib.contenttypes.admin.GenericInlineModelAdmin`.

This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.

CVE-2026-4292: Privilege abuse in ``ModelAdmin.list_editable``
==============================================================

Admin changelist forms using
:attr:`~django.contrib.admin.ModelAdmin.list_editable` incorrectly allowed new
instances to be created via forged ``POST`` data.

This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.

CVE-2026-33033: Potential denial-of-service vulnerability in ``MultiPartParser`` via base64-encoded file upload
===============================================================================================================

When using ``django.http.multipartparser.MultiPartParser``, multipart uploads
with ``Content-Transfer-Encoding: base64`` that include excessive whitespace
may trigger repeated memory copying, potentially degrading performance.

This issue has severity "moderate" according to the :ref:`Django security
policy <security-disclosure>`.

CVE-2026-33034: Potential denial-of-service vulnerability in ASGI requests via memory upload limit bypass
=========================================================================================================

ASGI requests with a missing or understated ``Content-Length`` header could
bypass the :setting:`DATA_UPLOAD_MAX_MEMORY_SIZE` limit when reading
``HttpRequest.body``, potentially loading an unbounded request body into
memory and causing service degradation.

This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.
Loading
Loading