From a1473d1dfceecfc82047508e2de026fb97ab738f Mon Sep 17 00:00:00 2001 From: Emily Bain Date: Thu, 19 Feb 2026 16:24:52 -0500 Subject: [PATCH] Update Django-Security to support Django 5.2 * Fixed some broken behaviour with middlewares that caused issues under 5.2 * Update version support for Django * bump version to 5.1.6 --- poetry.lock | 71 ++++++++++++++++++++---------------------- pyproject.toml | 4 +-- security/middleware.py | 4 +-- tests/tests.py | 44 ++++++++++++++------------ 4 files changed, 61 insertions(+), 62 deletions(-) diff --git a/poetry.lock b/poetry.lock index ff8fbe1..fec86b3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,13 +13,13 @@ files = [ [[package]] name = "asgiref" -version = "3.11.0" +version = "3.11.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.9" files = [ - {file = "asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d"}, - {file = "asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4"}, + {file = "asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133"}, + {file = "asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce"}, ] [package.extras] @@ -38,13 +38,13 @@ files = [ [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, + {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, + {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, ] [package.extras] @@ -300,17 +300,17 @@ files = [ [[package]] name = "django" -version = "4.2.27" +version = "5.2.11" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "django-4.2.27-py3-none-any.whl", hash = "sha256:f393a394053713e7d213984555c5b7d3caeee78b2ccb729888a0774dff6c11a8"}, - {file = "django-4.2.27.tar.gz", hash = "sha256:b865fbe0f4a3d1ee36594c5efa42b20db3c8bbb10dff0736face1c6e4bda5b92"}, + {file = "django-5.2.11-py3-none-any.whl", hash = "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0"}, + {file = "django-5.2.11.tar.gz", hash = "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3"}, ] [package.dependencies] -asgiref = ">=3.6.0,<4" +asgiref = ">=3.8.1" sqlparse = ">=0.3.1" tzdata = {version = "*", markers = "sys_platform == \"win32\""} @@ -345,13 +345,13 @@ files = [ [[package]] name = "filelock" -version = "3.20.3" +version = "3.24.3" description = "A platform independent file lock." optional = false python-versions = ">=3.10" files = [ - {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, - {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, + {file = "filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d"}, + {file = "filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa"}, ] [[package]] @@ -669,24 +669,24 @@ files = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, ] [[package]] name = "pathspec" -version = "1.0.3" +version = "1.0.4" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.9" files = [ - {file = "pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c"}, - {file = "pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d"}, + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, ] [package.extras] @@ -697,20 +697,15 @@ tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.9.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" files = [ - {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, - {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, + {file = "platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd"}, + {file = "platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291"}, ] -[package.extras] -docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] -type = ["mypy (>=1.18.2)"] - [[package]] name = "pre-commit" version = "3.7.1" @@ -912,13 +907,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "14.2.0" +version = "14.3.3" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" files = [ - {file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"}, - {file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"}, + {file = "rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d"}, + {file = "rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b"}, ] [package.dependencies] @@ -1199,25 +1194,25 @@ zstd = ["backports-zstd (>=1.0.0)"] [[package]] name = "virtualenv" -version = "20.36.1" +version = "20.38.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ - {file = "virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f"}, - {file = "virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba"}, + {file = "virtualenv-20.38.0-py3-none-any.whl", hash = "sha256:d6e78e5889de3a4742df2d3d44e779366325a90cf356f15621fddace82431794"}, + {file = "virtualenv-20.38.0.tar.gz", hash = "sha256:94f39b1abaea5185bf7ea5a46702b56f1d0c9aa2f41a6c2b8b0af4ddc74c10a7"}, ] [package.dependencies] distlib = ">=0.3.7,<1" -filelock = {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""} +filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +docs = ["furo (>=2023.7.26)", "pre-commit-uv (>=4.1.4)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinx-autodoc-typehints (>=3.6.2)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2025.12.21.14)", "sphinxcontrib-mermaid (>=2)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "pytest-xdist (>=3.5)", "setuptools (>=68)", "time-machine (>=2.10)"] [metadata] lock-version = "2.0" python-versions = "~3.12" -content-hash = "95b715b4b6b4316eaa0f1058dcd648a21d0952afe7a0b0588dd6c16759be8d88" +content-hash = "61235f00cbcdfccfae239f343e92e698cb8a5f58779ba660b74fc88cd701c97c" diff --git a/pyproject.toml b/pyproject.toml index 4391a67..b599720 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-security" -version = "1.1.5" +version = "1.1.6" homepage = "https://github.com/sdelements/django-security" description = "Models, views, middlewares and forms to facilitate security hardening of Django applications." authors = ["Security Compass "] @@ -39,7 +39,7 @@ exclude = [ [tool.poetry.dependencies] python = "~3.12" -django = "~4.2" +django = ">=4.2, <6.0" python-dateutil = "2.9.0.post0" south = "1.0.2" ua_parser = "0.18.0" diff --git a/security/middleware.py b/security/middleware.py index 61856bd..6f796c8 100644 --- a/security/middleware.py +++ b/security/middleware.py @@ -105,7 +105,7 @@ def _on_setting_changed(self, sender, setting, value, **kwargs): self.load_setting(setting, value) def __init__(self, get_response=None): - self.get_response = get_response + super().__init__(get_response) if not self.REQUIRED_SETTINGS and not self.OPTIONAL_SETTINGS: return @@ -701,7 +701,7 @@ def _csp_builder(self, csp_dict): def __init__(self, get_response=None): # sanity checks - self.get_response = get_response + super().__init__(get_response=get_response) conf_csp_mode = getattr(django.conf.settings, "CSP_MODE", None) self._csp_mode = conf_csp_mode or "enforce" diff --git a/tests/tests.py b/tests/tests.py index b4c8736..b82a84f 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -46,6 +46,7 @@ from mock import MagicMock mocked_custom_logout = MagicMock(side_effect=logout) +mocked_get_response = MagicMock() def login_user(func): @@ -134,7 +135,7 @@ def test_setting_change(self): self.assertEqual(123, response.loaded_settings["R1"]) def test_load_setting_abstract_method(self): - base = BaseMiddleware() + base = BaseMiddleware(mocked_get_response) self.assertRaises(NotImplementedError, base.load_setting, None, None) @@ -328,7 +329,7 @@ def test_dont_choke_on_exempt_urls_that_dont_resolve(self): user.delete() def test_raises_improperly_configured(self): - change = MandatoryPasswordChangeMiddleware() + change = MandatoryPasswordChangeMiddleware(mocked_get_response) self.assertRaises( ImproperlyConfigured, change.load_setting, @@ -400,7 +401,7 @@ def test_session_too_old(self): Pretend we are 1 second passed the session age time and make sure out session is cleared. """ - delta = SessionExpiryPolicyMiddleware().SESSION_COOKIE_AGE + 1 + delta = SessionExpiryPolicyMiddleware(mocked_get_response).SESSION_COOKIE_AGE + 1 expired = timezone.now() - datetime.timedelta(seconds=delta) self.session_expiry_test(SessionExpiryPolicyMiddleware.START_TIME_KEY, expired) @@ -410,16 +411,16 @@ def test_session_inactive_too_long(self): Pretend we are 1 second passed the session inactivity timeout and make sure the session is cleared. """ - delta = SessionExpiryPolicyMiddleware().SESSION_INACTIVITY_TIMEOUT + 1 + delta = SessionExpiryPolicyMiddleware(mocked_get_response).SESSION_INACTIVITY_TIMEOUT + 1 expired = timezone.now() - datetime.timedelta(seconds=delta) self.session_expiry_test( - SessionExpiryPolicyMiddleware().LAST_ACTIVITY_KEY, + SessionExpiryPolicyMiddleware(mocked_get_response).LAST_ACTIVITY_KEY, expired, ) @login_user def test_exempted_session_expiry_urls(self): - delta = SessionExpiryPolicyMiddleware().SESSION_INACTIVITY_TIMEOUT + 1 + delta = SessionExpiryPolicyMiddleware(mocked_get_response).SESSION_INACTIVITY_TIMEOUT + 1 expired = timezone.now() - datetime.timedelta(seconds=delta) self.assertTrue(self.client.get("/home/").status_code, 200) @@ -438,10 +439,10 @@ def test_exempted_session_expiry_urls(self): @login_user def test_custom_logout(self): - delta = SessionExpiryPolicyMiddleware().SESSION_INACTIVITY_TIMEOUT + 1 + delta = SessionExpiryPolicyMiddleware(mocked_get_response).SESSION_INACTIVITY_TIMEOUT + 1 expired = timezone.now() - datetime.timedelta(seconds=delta) self.session_expiry_test( - SessionExpiryPolicyMiddleware().LAST_ACTIVITY_KEY, + SessionExpiryPolicyMiddleware(mocked_get_response).LAST_ACTIVITY_KEY, expired, ) assert mocked_custom_logout.called @@ -513,7 +514,7 @@ def test_exclude_urls(self): self.assertNotIn("X-Frame-Options", response) def test_improperly_configured(self): - xframe = XFrameOptionsMiddleware() + xframe = XFrameOptionsMiddleware(mocked_get_response) self.assertRaises( ImproperlyConfigured, xframe.load_setting, @@ -718,7 +719,7 @@ def test_reset_button(self): } ) def test_improperly_configured_middleware(self): - self.assertRaises(ImproperlyConfigured, AuthThrottlingMiddleware) + self.assertRaises(ImproperlyConfigured, AuthThrottlingMiddleware, mocked_get_response) def test_throttle_reset_404_on_unauthorized(self): resp = self.client.post( @@ -844,7 +845,7 @@ def test_csp_gen_1(self): "font-src fonts.example.com" ) - csp = ContentSecurityPolicyMiddleware() + csp = ContentSecurityPolicyMiddleware(mocked_get_response) generated = csp._csp_builder(csp_dict) # We can't assume the iteration order on the csp_dict, so we split the @@ -859,7 +860,7 @@ def test_csp_gen_2(self): csp_dict = {"default-src": ("none",), "script-src": ["none"]} expected = "default-src 'none'; script-src 'none'" - csp = ContentSecurityPolicyMiddleware() + csp = ContentSecurityPolicyMiddleware(mocked_get_response) generated = csp._csp_builder(csp_dict) expected_list = sorted(x.strip() for x in expected.split(";")) @@ -878,7 +879,7 @@ def test_csp_gen_3(self): expected = "script-src " "'self' www.google-analytics.com ajax.googleapis.com" - csp = ContentSecurityPolicyMiddleware() + csp = ContentSecurityPolicyMiddleware(mocked_get_response) generated = csp._csp_builder(csp_dict) self.assertEqual(generated, expected) @@ -887,40 +888,40 @@ def test_csp_gen_err(self): # argument not passed as array, expect failure csp_dict = {"default-src": "self"} - csp = ContentSecurityPolicyMiddleware() + csp = ContentSecurityPolicyMiddleware(mocked_get_response) self.assertRaises(MiddlewareNotUsed, csp._csp_builder, csp_dict) def test_csp_gen_err2(self): csp_dict = {"invalid": "self"} # invalid directive - csp = ContentSecurityPolicyMiddleware() + csp = ContentSecurityPolicyMiddleware(mocked_get_response) self.assertRaises(MiddlewareNotUsed, csp._csp_builder, csp_dict) def test_csp_gen_err3(self): csp_dict = {"sandbox": "none"} # not a list or tuple, expect failure - csp = ContentSecurityPolicyMiddleware() + csp = ContentSecurityPolicyMiddleware(mocked_get_response) self.assertRaises(MiddlewareNotUsed, csp._csp_builder, csp_dict) def test_csp_gen_err4(self): # Not an allowed directive, expect failure csp_dict = {"sandbox": ("invalid",)} - csp = ContentSecurityPolicyMiddleware() + csp = ContentSecurityPolicyMiddleware(mocked_get_response) self.assertRaises(MiddlewareNotUsed, csp._csp_builder, csp_dict) def test_csp_gen_err5(self): # Not an allowed directive, expect failure csp_dict = {"referrer": "invalid"} - csp = ContentSecurityPolicyMiddleware() + csp = ContentSecurityPolicyMiddleware(mocked_get_response) self.assertRaises(MiddlewareNotUsed, csp._csp_builder, csp_dict) def test_csp_gen_err6(self): # Not an allowed directive, expect failure csp_dict = {"reflected-xss": "invalid"} - csp = ContentSecurityPolicyMiddleware() + csp = ContentSecurityPolicyMiddleware(mocked_get_response) self.assertRaises(MiddlewareNotUsed, csp._csp_builder, csp_dict) def test_enforced_by_default(self): @@ -952,6 +953,7 @@ def test_invalid_csp_mode(self): self.assertRaises( MiddlewareNotUsed, ContentSecurityPolicyMiddleware, + mocked_get_response ) def test_no_csp_options_set(self): @@ -959,6 +961,7 @@ def test_no_csp_options_set(self): self.assertRaises( MiddlewareNotUsed, ContentSecurityPolicyMiddleware, + mocked_get_response ) def test_both_csp_options_set(self): @@ -966,6 +969,7 @@ def test_both_csp_options_set(self): self.assertRaises( MiddlewareNotUsed, ContentSecurityPolicyMiddleware, + mocked_get_response ) def test_sets_from_csp_dict(self): @@ -1080,7 +1084,7 @@ def test_off_setting(self): self.assertEqual("Referrer-Policy" in response, False) def test_improper_configuration_raises(self): - referer_policy_middleware = ReferrerPolicyMiddleware() + referer_policy_middleware = ReferrerPolicyMiddleware(mocked_get_response) self.assertRaises( ImproperlyConfigured, referer_policy_middleware.load_setting,