diff --git a/README.md b/README.md
index 78749a6..81b673a 100644
--- a/README.md
+++ b/README.md
@@ -119,6 +119,11 @@ or minimum configuration.
SessionExpiryPolicyMiddleware
| Expire sessions on browser close, and on expiry times stored in the cookie itself.
diff --git a/security/middleware.py b/security/middleware.py
index 98a0573..af50896 100644
--- a/security/middleware.py
+++ b/security/middleware.py
@@ -17,7 +17,7 @@
from django.utils.deprecation import MiddlewareMixin
import django.views.static
-from ua_parser.user_agent_parser import ParseUserAgent
+from ua_parser import user_agent_parser
logger = logging.getLogger(__name__)
@@ -901,7 +901,7 @@ def process_response(self, request, response):
# choose headers based enforcement mode
is_ie = False
if 'HTTP_USER_AGENT' in request.META:
- parsed_ua = ParseUserAgent(request.META['HTTP_USER_AGENT'])
+ parsed_ua = user_agent_parser.ParseUserAgent(request.META['HTTP_USER_AGENT'])
is_ie = parsed_ua['family'] == 'IE'
csp_header = 'Content-Security-Policy'
@@ -1315,3 +1315,56 @@ def process_request(self, request):
login_url = login_url + '?next=' + next_url
return HttpResponseRedirect(login_url)
+
+class ReferrerPolicyMiddleware(BaseMiddleware):
+ """
+ Sends Referrer-Policy HTTP header that controls when the browser will set
+ the `Referer` header. Use REFERRER_POLICY option in settings file
+ with the following values:
+
+ - ``no-referrer``
+ - ``no-referrer-when-downgrade``
+ - ``origin``
+ - ``origin-when-cross-origin``
+ - ``same-origin`` (*default*)
+ - ``strict-origin``
+ - ``strict-origin-when-cross-origin``
+ - ``unsafe-url``
+ - ``off``
+
+ Reference:
+ - `Referrer-Policy from Mozilla Developer Network
+ `
+ """
+
+ OPTIONAL_SETTINGS = ("REFERRER_POLICY",)
+
+ OPTIONS = [ 'no-referrer', 'no-referrer-when-downgrade', 'origin',
+ 'origin-when-cross-origin', 'same-origin', 'strict-origin',
+ 'strict-origin-when-cross-origin', 'unsafe-url', 'off' ]
+
+ DEFAULT = 'same-origin'
+
+ def load_setting(self, setting, value):
+ if not value:
+ self.option = self.DEFAULT
+ return
+
+ value = value.lower()
+
+ if value in self.OPTIONS:
+ self.option = value
+ return
+
+ raise ImproperlyConfigured(
+ self.__class__.__name__ + " invalid option for REFERRER_POLICY."
+ )
+
+ def process_response(self, request, response):
+ """
+ Add Referrer-Policy to the reponse header.
+ """
+ if self.option != 'off':
+ header = self.option
+ response['Referrer-Policy'] = header
+ return response
diff --git a/setup.py b/setup.py
index 78c15fb..d02ffb5 100644
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@
from distutils.core import Command
from setuptools import setup
-with open(os.path.join(os.path.dirname(__file__), 'README.md')) as f:
+with open(os.path.join(os.path.dirname(__file__), "README.md")) as f:
readme = f.read()
@@ -20,37 +20,44 @@ def finalize_options(self):
pass
def run(self):
- errno = subprocess.call([sys.executable, 'testing/manage.py', 'test'])
+ errno = subprocess.call([sys.executable, "testing/manage.py", "test"])
raise SystemExit(errno)
-setup(name="django-security",
- description='A collection of tools to help secure a Django project.',
- long_description=readme,
- long_description_content_type='text/markdown',
- maintainer="SD Elements",
- maintainer_email="django-security@sdelements.com",
- version="0.13.2",
- packages=["security", "security.south_migrations",
- "security.migrations", "security.auth_throttling"],
- url='https://github.com/sdelements/django-security',
- classifiers=[
- 'Framework :: Django',
- 'Framework :: Django :: 1.11',
- 'Framework :: Django :: 2.2',
- 'Framework :: Django :: 3.0',
- 'Environment :: Web Environment',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.6',
- 'Intended Audience :: Developers',
- 'Operating System :: OS Independent',
- 'License :: OSI Approved :: BSD License',
- 'Topic :: Software Development :: Libraries :: Python Modules',
- 'Topic :: Security',
- ],
- install_requires=[
- 'django>=1.11',
- 'ua_parser>=0.7.1',
- 'python-dateutil==2.8.1',
- ],
- cmdclass={'test': Test})
+
+setup(
+ name="django-security",
+ description="A collection of tools to help secure a Django project.",
+ long_description=readme,
+ long_description_content_type="text/markdown",
+ maintainer="SD Elements",
+ maintainer_email="django-security@sdelements.com",
+ version="0.13.2",
+ packages=[
+ "security",
+ "security.south_migrations",
+ "security.migrations",
+ "security.auth_throttling",
+ ],
+ url="https://github.com/sdelements/django-security",
+ classifiers=[
+ "Framework :: Django",
+ "Framework :: Django :: 1.11",
+ "Framework :: Django :: 2.2",
+ "Framework :: Django :: 3.0",
+ "Environment :: Web Environment",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.6",
+ "Intended Audience :: Developers",
+ "Operating System :: OS Independent",
+ "License :: OSI Approved :: BSD License",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: Security",
+ ],
+ install_requires=[
+ "django>=1.11",
+ "ua_parser>=0.7.1",
+ "python-dateutil==2.8.1",
+ ],
+ cmdclass={"test": Test},
+)
diff --git a/testing/settings.py b/testing/settings.py
index f81ce2d..a81213b 100644
--- a/testing/settings.py
+++ b/testing/settings.py
@@ -50,6 +50,7 @@
'security.middleware.MandatoryPasswordChangeMiddleware',
'security.middleware.NoConfidentialCachingMiddleware',
'security.auth_throttling.Middleware',
+ 'security.middleware.ReferrerPolicyMiddleware',
)
ROOT_URLCONF = 'testing.urls'
diff --git a/testing/tests/tests.py b/testing/tests/tests.py
index d9b5e02..3ce346d 100644
--- a/testing/tests/tests.py
+++ b/testing/tests/tests.py
@@ -24,7 +24,7 @@
from security.middleware import (
BaseMiddleware, ContentSecurityPolicyMiddleware, DoNotTrackMiddleware,
SessionExpiryPolicyMiddleware, MandatoryPasswordChangeMiddleware,
- XssProtectMiddleware, XFrameOptionsMiddleware,
+ XssProtectMiddleware, XFrameOptionsMiddleware, ReferrerPolicyMiddleware
)
from security.models import PasswordExpiry
from security.password_expiry import never_expire_password
@@ -1064,24 +1064,70 @@ def test_DNT_echo_default(self):
self.dnt.process_response(self.request, self.response)
self.assertNotIn('DNT', self.response)
+class ReferrerPolicyTests(TestCase):
-@override_settings(MIDDLEWARE=(
- 'security.middleware.ClearSiteDataMiddleware',
-))
-class ClearSiteDataMiddlewareTests(TestCase):
- def test_request_that_matches_the_whitelist_with_default_directives(self):
- response = self.client.get('/home/')
- self.assertEqual(response['Clear-Site-Data'], '"cookies", "storage"')
+ def test_option_set(self):
+ """
+ Verify the HTTP Referrer-Policy Header is set.
+ """
+ response = self.client.get('/accounts/login/')
+ self.assertNotEqual(response['Referrer-Policy'], None)
- def test_request_that_misses_the_whitelist(self):
- response = self.client.get('/test1/')
- self.assertNotIn("Clear-Site-Data", response)
+ def test_default_setting(self):
+ with self.settings(REFERRER_POLICY=None):
+ response = self.client.get('/accounts/login/')
+ self.assertEqual(response['Referrer-Policy'], 'same-origin')
- @override_settings(CLEAR_SITE_DATA_DIRECTIVES=(
- 'cache', 'cookies', 'executionContexts', '*'
- ))
- def test_request_that_matches_the_whitelist_with_custom_directives(self):
- response = self.client.get('/home/')
- self.assertEqual(
- response['Clear-Site-Data'],
- '"cache", "cookies", "executionContexts", "*"')
+ def test_no_referrer_setting(self):
+ with self.settings(REFERRER_POLICY='no-referrer'):
+ response = self.client.get('/accounts/login/')
+ self.assertEqual(response['Referrer-Policy'], 'no-referrer')
+
+ def test_no_referrer_when_downgrade_setting(self):
+ with self.settings(REFERRER_POLICY='no-referrer-when-downgrade'):
+ response = self.client.get('/accounts/login/')
+ self.assertEqual(response['Referrer-Policy'], 'no-referrer-when-downgrade')
+
+ def test_origin_setting(self):
+ with self.settings(REFERRER_POLICY='origin'):
+ response = self.client.get('/accounts/login/')
+ self.assertEqual(response['Referrer-Policy'], 'origin')
+
+ def test_origin_when_cross_origin_setting(self):
+ with self.settings(REFERRER_POLICY='origin-when-cross-origin'):
+ response = self.client.get('/accounts/login/')
+ self.assertEqual(response['Referrer-Policy'], 'origin-when-cross-origin')
+
+ def test_same_origin_setting(self):
+ with self.settings(REFERRER_POLICY='same-origin'):
+ response = self.client.get('/accounts/login/')
+ self.assertEqual(response['Referrer-Policy'], 'same-origin')
+
+ def test_strict_origin_setting(self):
+ with self.settings(REFERRER_POLICY='strict-origin'):
+ response = self.client.get('/accounts/login/')
+ self.assertEqual(response['Referrer-Policy'], 'strict-origin')
+
+ def test_strict_origin_when_cross_origin_setting(self):
+ with self.settings(REFERRER_POLICY='strict-origin-when-cross-origin'):
+ response = self.client.get('/accounts/login/')
+ self.assertEqual(response['Referrer-Policy'], 'strict-origin-when-cross-origin')
+
+ def test_unsafe_url_setting(self):
+ with self.settings(REFERRER_POLICY='unsafe-url'):
+ response = self.client.get('/accounts/login/')
+ self.assertEqual(response['Referrer-Policy'], 'unsafe-url')
+
+ def test_off_setting(self):
+ with self.settings(REFERRER_POLICY='off'):
+ response = self.client.get('/accounts/login/')
+ self.assertEqual('Referrer-Policy' in response, False)
+
+ def test_improper_configuration_raises(self):
+ referer_policy_middleware = ReferrerPolicyMiddleware()
+ self.assertRaises(
+ ImproperlyConfigured,
+ referer_policy_middleware.load_setting,
+ 'REFERRER_POLICY',
+ 'invalid',
+ )
|