diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index d4a8dd902b..623b169801 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -174,11 +174,15 @@ class UserManager(BaseUserManager): extra_fields.setdefault("is_superuser", False) return self._create_user(username, email, password, **extra_fields) + create_user.alters_data = True + async def acreate_user(self, username, email=None, password=None, **extra_fields): extra_fields.setdefault("is_staff", False) extra_fields.setdefault("is_superuser", False) return await self._acreate_user(username, email, password, **extra_fields) + acreate_user.alters_data = True + def create_superuser(self, username, email=None, password=None, **extra_fields): extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", True) @@ -190,6 +194,8 @@ class UserManager(BaseUserManager): return self._create_user(username, email, password, **extra_fields) + create_superuser.alters_data = True + async def acreate_superuser( self, username, email=None, password=None, **extra_fields ): @@ -203,6 +209,8 @@ class UserManager(BaseUserManager): return await self._acreate_user(username, email, password, **extra_fields) + acreate_superuser.alters_data = True + def with_perm( self, perm, is_active=True, include_superusers=True, backend=None, obj=None ): diff --git a/django/db/models/query.py b/django/db/models/query.py index a4277d05fc..21d5534cc9 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -660,9 +660,13 @@ class QuerySet(AltersData): obj.save(force_insert=True, using=self.db) return obj + create.alters_data = True + async def acreate(self, **kwargs): return await sync_to_async(self.create)(**kwargs) + acreate.alters_data = True + def _prepare_for_bulk_create(self, objs): from django.db.models.expressions import DatabaseDefault @@ -835,6 +839,8 @@ class QuerySet(AltersData): return objs + bulk_create.alters_data = True + async def abulk_create( self, objs, @@ -853,6 +859,8 @@ class QuerySet(AltersData): unique_fields=unique_fields, ) + abulk_create.alters_data = True + def bulk_update(self, objs, fields, batch_size=None): """ Update the given fields in each of the given objects in the database. @@ -941,12 +949,16 @@ class QuerySet(AltersData): pass raise + get_or_create.alters_data = True + async def aget_or_create(self, defaults=None, **kwargs): return await sync_to_async(self.get_or_create)( defaults=defaults, **kwargs, ) + aget_or_create.alters_data = True + def update_or_create(self, defaults=None, create_defaults=None, **kwargs): """ Look up an object with the given kwargs, updating one with defaults @@ -992,6 +1004,8 @@ class QuerySet(AltersData): obj.save(using=self.db) return obj, False + update_or_create.alters_data = True + async def aupdate_or_create(self, defaults=None, create_defaults=None, **kwargs): return await sync_to_async(self.update_or_create)( defaults=defaults, @@ -999,6 +1013,8 @@ class QuerySet(AltersData): **kwargs, ) + aupdate_or_create.alters_data = True + def _extract_model_params(self, defaults, **kwargs): """ Prepare `params` for creating a model instance based on the given diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt index 96007887bc..8327de7405 100644 --- a/docs/releases/5.2.txt +++ b/docs/releases/5.2.txt @@ -392,6 +392,22 @@ Miscellaneous * The :func:`~django.template.context_processors.debug` context processor is no longer included in the default project template. +* The following methods now have ``alters_data=True`` set to prevent side + effects when :ref:`rendering a template context `: + + * :meth:`.UserManager.create_user` + * :meth:`.UserManager.acreate_user` + * :meth:`.UserManager.create_superuser` + * :meth:`.UserManager.acreate_superuser` + * :meth:`.QuerySet.create` + * :meth:`.QuerySet.acreate` + * :meth:`.QuerySet.bulk_create` + * :meth:`.QuerySet.abulk_create` + * :meth:`.QuerySet.get_or_create` + * :meth:`.QuerySet.aget_or_create` + * :meth:`.QuerySet.update_or_create` + * :meth:`.QuerySet.aupdate_or_create` + .. _deprecated-features-5.2: Features deprecated in 5.2 diff --git a/tests/template_backends/test_jinja2.py b/tests/template_backends/test_jinja2.py index 55c9299f85..508971f581 100644 --- a/tests/template_backends/test_jinja2.py +++ b/tests/template_backends/test_jinja2.py @@ -1,8 +1,9 @@ from pathlib import Path from unittest import mock, skipIf +from django.contrib.auth.models import User from django.template import TemplateSyntaxError -from django.test import RequestFactory +from django.test import RequestFactory, TestCase from .test_dummy import TemplateStringsTests @@ -135,3 +136,31 @@ class Jinja2Tests(TemplateStringsTests): self.assertEqual(len(debug["source_lines"]), 0) self.assertTrue(debug["name"].endswith("nonexistent.html")) self.assertIn("message", debug) + + +@skipIf(jinja2 is None, "this test requires jinja2") +class Jinja2SandboxTests(TestCase): + engine_class = Jinja2 + backend_name = "jinja2" + options = {"environment": "jinja2.sandbox.SandboxedEnvironment"} + + @classmethod + def setUpClass(cls): + super().setUpClass() + params = { + "DIRS": [], + "APP_DIRS": True, + "NAME": cls.backend_name, + "OPTIONS": cls.options, + } + cls.engine = cls.engine_class(params) + + def test_set_alters_data(self): + template = self.engine.from_string( + "{% set test = User.objects.create_superuser(" + "username='evil', email='a@b.com', password='xxx') %}" + "{{ test }}" + ) + with self.assertRaises(jinja2.exceptions.SecurityError): + template.render(context={"User": User}) + self.assertEqual(User.objects.count(), 0)