diff --git a/django/contrib/sessions/backends/cached_db.py b/django/contrib/sessions/backends/cached_db.py index 2195f57acccd..a81d5789f240 100644 --- a/django/contrib/sessions/backends/cached_db.py +++ b/django/contrib/sessions/backends/cached_db.py @@ -109,7 +109,10 @@ def delete(self, session_key=None): if self.session_key is None: return session_key = self.session_key - self._cache.delete(self.cache_key_prefix + session_key) + try: + self._cache.delete(self.cache_key_prefix + session_key) + except Exception: + logger.exception("Error deleting from cache (%s)", self._cache) async def adelete(self, session_key=None): await super().adelete(session_key) @@ -117,7 +120,10 @@ async def adelete(self, session_key=None): if self.session_key is None: return session_key = self.session_key - await self._cache.adelete(self.cache_key_prefix + session_key) + try: + await self._cache.adelete(self.cache_key_prefix + session_key) + except Exception: + logger.exception("Error deleting from cache (%s)", self._cache) def flush(self): """ diff --git a/docs/howto/deployment/asgi/uvicorn.txt b/docs/howto/deployment/asgi/uvicorn.txt index b02d6c20ee94..ff273b426590 100644 --- a/docs/howto/deployment/asgi/uvicorn.txt +++ b/docs/howto/deployment/asgi/uvicorn.txt @@ -56,5 +56,5 @@ Then start Gunicorn using the Uvicorn worker class like this: python -m gunicorn myproject.asgi:application -k uvicorn_worker.UvicornWorker -.. _Uvicorn: https://www.uvicorn.org/ +.. _Uvicorn: https://www.uvicorn.dev/ .. _Gunicorn: https://gunicorn.org/ diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index 6070b3e4ef40..f69cb918b751 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -77,9 +77,9 @@ cache or a non-persistent cache. The cached database backend (``cached_db``) uses a write-through cache -- session writes are applied to both the database and cache, in that order. If -writing to the cache fails, the exception is handled and logged via the -:ref:`sessions logger `, to avoid failing an -otherwise successful write operation. +writing to the cache (or deleting from it) fails, the exception is handled and +logged via the :ref:`sessions logger `, to +avoid failing an otherwise successful write or delete operation. Session reads use the cache, or the database if the data has been evicted from the cache. To use this backend, set :setting:`SESSION_ENGINE` to @@ -98,6 +98,11 @@ Redis with appropriate configuration. But unless your cache is definitely configured for sufficient persistence, opt for the cached database backend. This avoids edge cases caused by unreliable data storage in production. +.. versionchanged:: 6.2 + + In earlier versions, only failed writes were caught and logged rather than + failed deletes. + Using file-based sessions ------------------------- diff --git a/docs/topics/security.txt b/docs/topics/security.txt index 3a3d79b8a8fb..dbb7a2e47788 100644 --- a/docs/topics/security.txt +++ b/docs/topics/security.txt @@ -5,6 +5,19 @@ Security in Django This document is an overview of Django's security features. It includes advice on securing a Django-powered site. +.. admonition:: Real-world security + + Django's web security implementations have been designed with security for + real-world applications in mind. Django is a general-purpose web + application framework, and its defaults reflect this - they will not be the + best solution for every particular case. Special cases deserve special + attention to their needs. + + Web security requires a multi-layered approach. Securing only a single + vector does not make a site secure overall, nor does a single apparent + weakness necessarily compromise the entire site. This should be borne in + mind particularly when assessing individual points in security audits. + .. _sanitize-user-input: Always sanitize user input @@ -17,25 +30,35 @@ details on validating user inputs in Django. .. _cross-site-scripting: -Cross site scripting (XSS) protection +Cross-site scripting (XSS) protection ===================================== -XSS attacks allow a user to inject client side scripts into the browsers of -other users. This is usually achieved by storing the malicious scripts in the -database where it will be retrieved and displayed to other users, or by getting -users to click a link which will cause the attacker's JavaScript to be executed -by the user's browser. However, XSS attacks can originate from any untrusted -source of data, such as cookies or web services, whenever the data is not -sufficiently sanitized before including in a page. +In a cross-site scripting attack, malicious code in the form of a client-side +script is injected into another user's web browser, where it will be executed. + +This is typically done by: + +* storing the malicious script in the database where it will be retrieved and + presented to other users in their browsers, or +* getting users to click a link which will cause the attacker's JavaScript to + be executed by the user's browser. -Using Django templates protects you against the majority of XSS attacks. -However, it is important to understand what protections it provides -and its limitations. +Cross-site scripting attacks can originate from any untrusted source of data, +including cookies or web services, if the data are not adequately sanitized +before being published in a page. -Django templates :ref:`escape specific characters ` -which are particularly dangerous to HTML. While this protects users from most -malicious input, it is not entirely foolproof. For example, it will not -protect the following: +Django templates provide protection against the majority of cross-site +scripting attacks by :ref:`automatically escaping characters that represent a +risk ` (that is, HTML characters that could be +*interpreted* by the browser to malicious effect are instead safely +*displayed*). + +However, the extent of this protection and its limitations should be +understood. + +Ambiguity between the developer's intention and how the browser interprets +HTML can expose the client to unintended code execution. Suppose that a +developer creates: .. code-block:: text @@ -43,23 +66,34 @@ protect the following: .. highlighting as html+django fails due to intentionally missing quotes. -If ``var`` is set to ``'class1 onmouseover=javascript:func()'``, this can -result in unauthorized JavaScript execution, depending on how the browser -renders imperfect HTML. (Quoting the attribute value would fix this case.) +where ``var`` is expected to contain something like ``'class1'``. If ``var`` +were set to ``'class1 onmouseover=javascript:func()'`` though, this could +result in unauthorized JavaScript execution due to differences in how browsers +will interpret this imperfect HTML. + +Explicit template design, in which the quotes are not left to the variable to +provide: + +.. code-block:: html+django + + -It is also important to be particularly careful when using ``is_safe`` with -custom template tags, the :tfilter:`safe` template tag, :mod:`mark_safe -`, and when autoescape is turned off. +would eliminate this possibility. -In addition, if you are using the template system to output something other -than HTML, there may be entirely separate characters and words which require -escaping. +It is also important to be particularly careful when using the ``is_safe`` +attribute with custom template tags, the :tfilter:`safe` template tag, +:mod:`mark_safe `, and when autoescape is turned off. -You should also be very careful when storing HTML in the database, especially -when that HTML is retrieved and displayed. +Django's built-in escaping is intended to protect HTML output. If you are using +the template system to output something other than HTML, the characters and +strings that require escaping might be entirely different. +Additionally, be very careful when storing HTML in the database, especially +when that HTML is retrieved and displayed. Unless the HTML is guaranteed to +come from a trusted source - user input is *not* a trusted source - stored HTML +should be checked and sanitized, preferably on input as well as output. -Cross site request forgery (CSRF) protection +Cross-site request forgery (CSRF) protection ============================================ CSRF attacks allow a malicious user to execute actions using the credentials @@ -182,8 +216,8 @@ Host header validation ====================== Django uses the ``Host`` header provided by the client to construct URLs in -certain cases. While these values are sanitized to prevent Cross Site Scripting -attacks, a fake ``Host`` value can be used for Cross-Site Request Forgery, +certain cases. While these values are sanitized to prevent cross-site scripting +attacks, a fake ``Host`` value can be used for cross-site request forgery, cache poisoning attacks, and poisoning links in emails. Because even seemingly-secure web server configurations are susceptible to fake diff --git a/tests/cache/failing_cache.py b/tests/cache/failing_cache.py index 1c9b5996d61f..089d732591e0 100644 --- a/tests/cache/failing_cache.py +++ b/tests/cache/failing_cache.py @@ -8,3 +8,9 @@ def set(self, *args, **kwargs): async def aset(self, *args, **kwargs): raise Exception("Faked exception saving to cache") + + def delete(self, *args, **kwargs): + raise Exception("Faked exception deleting from cache") + + async def adelete(self, *args, **kwargs): + raise Exception("Faked exception deleting from cache") diff --git a/tests/gis_tests/gdal_tests/test_raster.py b/tests/gis_tests/gdal_tests/test_raster.py index d801317d5234..ca7251914b8d 100644 --- a/tests/gis_tests/gdal_tests/test_raster.py +++ b/tests/gis_tests/gdal_tests/test_raster.py @@ -151,6 +151,7 @@ def test_memory_based_raster_creation(self): def test_file_based_raster_creation(self): # Prepare tempfile rstfile = NamedTemporaryFile(suffix=".tif") + self.addCleanup(rstfile.close) # Create file-based raster from scratch GDALRaster( @@ -273,6 +274,7 @@ def test_vsi_buffer_length(self): def test_vsi_vsizip_filesystem(self): rst_zipfile = NamedTemporaryFile(suffix=".zip") + self.addCleanup(rst_zipfile.close) with zipfile.ZipFile(rst_zipfile, mode="w") as zf: zf.write(self.rs_path, "raster.tif") rst_path = "/vsizip/" + os.path.join(rst_zipfile.name, "raster.tif") @@ -416,6 +418,7 @@ def test_raster_info_accessor(self): def test_compressed_file_based_raster_creation(self): rstfile = NamedTemporaryFile(suffix=".tif") + self.addCleanup(rstfile.close) # Make a compressed copy of an existing raster. compressed = self.rs.warp( {"papsz_options": {"compress": "packbits"}, "name": rstfile.name} @@ -573,6 +576,7 @@ def test_raster_warp_nodata_zone(self): def test_raster_clone(self): rstfile = NamedTemporaryFile(suffix=".tif") + self.addCleanup(rstfile.close) tests = [ ("MEM", "", 23), # In memory raster. ("tif", rstfile.name, 99), # In file based raster. @@ -619,6 +623,7 @@ def test_raster_transform(self): with self.subTest(srs=srs): # Prepare tempfile and nodata value. rstfile = NamedTemporaryFile(suffix=".tif") + self.addCleanup(rstfile.close) ndv = 99 # Create in file based raster. source = GDALRaster( @@ -720,6 +725,7 @@ def test_raster_transform_clone(self): with mock.patch.object(GDALRaster, "clone") as mocked_clone: # Create in file based raster. rstfile = NamedTemporaryFile(suffix=".tif") + self.addCleanup(rstfile.close) source = GDALRaster( { "datatype": 1, @@ -748,6 +754,7 @@ def test_raster_transform_clone(self): def test_raster_transform_clone_name(self): # Create in file based raster. rstfile = NamedTemporaryFile(suffix=".tif") + self.addCleanup(rstfile.close) source = GDALRaster( { "datatype": 1, diff --git a/tests/inspectdb/tests.py b/tests/inspectdb/tests.py index b677af23df80..2a7cbc6bac94 100644 --- a/tests/inspectdb/tests.py +++ b/tests/inspectdb/tests.py @@ -662,6 +662,34 @@ def test_foreign_data_wrapper(self): self.assertIn(foreign_table_model, output) self.assertIn(foreign_table_managed, output) + @skipUnless(connection.vendor == "sqlite", "SQLite specific SQL") + @skipUnlessDBFeature("can_introspect_foreign_keys") + def test_foreign_key_to_sqlite_schema(self): + with connection.constraint_checks_disabled(): + cursor_execute(""" + CREATE TABLE inspectdb_sqlite_schema_fk ( + id INTEGER PRIMARY KEY, + table_name VARCHAR(64) + REFERENCES sqlite_schema (tbl_name), + content TEXT NOT NULL + ) + """) + + def cleanup(): + with connection.constraint_checks_disabled(): + cursor_execute("DROP TABLE IF EXISTS inspectdb_sqlite_schema_fk") + + self.addCleanup(cleanup) + out = StringIO() + call_command("inspectdb", "inspectdb_sqlite_schema_fk", stdout=out) + output = out.getvalue() + self.assertIn("class InspectdbSqliteSchemaFk(models.Model):", output) + self.assertIn( + "table_name = models.ForeignKey('SqliteSchema', models.DO_NOTHING, " + "db_column='table_name', blank=True, null=True)", + output, + ) + def test_composite_primary_key(self): out = StringIO() field_type = connection.features.introspected_field_types["IntegerField"] diff --git a/tests/sessions_tests/tests.py b/tests/sessions_tests/tests.py index 1a4b114a8a68..a7a07e5ea20b 100644 --- a/tests/sessions_tests/tests.py +++ b/tests/sessions_tests/tests.py @@ -842,6 +842,23 @@ def test_cache_set_failure_non_fatal(self): self.assertEqual(log.message, f"Error saving to cache ({session._cache})") self.assertEqual(str(log.exc_info[1]), "Faked exception saving to cache") + @override_settings( + CACHES={"default": {"BACKEND": "cache.failing_cache.CacheClass"}} + ) + def test_cache_delete_failure_non_fatal(self): + """Failing to delete from the cache does not raise errors.""" + session = self.backend() + session.save() + session_key = session.session_key + + with self.assertLogs("django.contrib.sessions", "ERROR") as cm: + session.delete(session_key) + + # A proper ERROR log message was recorded. + log = cm.records[-1] + self.assertEqual(log.message, f"Error deleting from cache ({session._cache})") + self.assertEqual(str(log.exc_info[1]), "Faked exception deleting from cache") + @override_settings( CACHES={"default": {"BACKEND": "cache.failing_cache.CacheClass"}} ) @@ -858,6 +875,23 @@ async def test_cache_async_set_failure_non_fatal(self): self.assertEqual(log.message, f"Error saving to cache ({session._cache})") self.assertEqual(str(log.exc_info[1]), "Faked exception saving to cache") + @override_settings( + CACHES={"default": {"BACKEND": "cache.failing_cache.CacheClass"}} + ) + async def test_cache_async_delete_failure_non_fatal(self): + """Failing to delete from the cache does not raise errors.""" + session = self.backend() + await session.asave() + session_key = session.session_key + + with self.assertLogs("django.contrib.sessions", "ERROR") as cm: + await session.adelete(session_key) + + # A proper ERROR log message was recorded. + log = cm.records[-1] + self.assertEqual(log.message, f"Error deleting from cache ({session._cache})") + self.assertEqual(str(log.exc_info[1]), "Faked exception deleting from cache") + @override_settings(USE_TZ=True) class CacheDBSessionWithTimeZoneTests(CacheDBSessionTests):