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
10 changes: 8 additions & 2 deletions django/contrib/sessions/backends/cached_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,21 @@ 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)
if session_key is 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):
"""
Expand Down
2 changes: 1 addition & 1 deletion docs/howto/deployment/asgi/uvicorn.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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/
11 changes: 8 additions & 3 deletions docs/topics/http/sessions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <django-contrib-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 <django-contrib-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
Expand All @@ -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
-------------------------

Expand Down
92 changes: 63 additions & 29 deletions docs/topics/security.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,49 +30,70 @@ 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 <automatic-html-escaping>`
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 <automatic-html-escaping>` (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

<style class={{ var }}>...</style>

.. 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

<style class="{{ var }}">...</style>

It is also important to be particularly careful when using ``is_safe`` with
custom template tags, the :tfilter:`safe` template tag, :mod:`mark_safe
<django.utils.safestring>`, 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 <django.utils.safestring>`, 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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions tests/cache/failing_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
7 changes: 7 additions & 0 deletions tests/gis_tests/gdal_tests/test_raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions tests/inspectdb/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
34 changes: 34 additions & 0 deletions tests/sessions_tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}
)
Expand All @@ -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):
Expand Down
Loading