Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Referrer-Policy #92

Merged
merged 1 commit into from
Jun 3, 2021
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ or minimum configuration.
<td><b>DEPRECATED: </b>Will be removed in future releases.<br/>Adds the HTTP header attribute specifying compact P3P policy.
<td>Required.

<tr>
<td><a href="http://django-security.readthedocs.org/en/latest/#security.middleware.ReferrerPolicyMiddleware">ReferrerPolicyMiddleware</a>
<td>Specify when the browser will set a `Referer` header.
<td>Optional.

<tr>
<td><a href="http://django-security.readthedocs.org/en/latest/#security.middleware.SessionExpiryPolicyMiddleware">SessionExpiryPolicyMiddleware</a>
<td>Expire sessions on browser close, and on expiry times stored in the cookie itself.
Expand Down
57 changes: 55 additions & 2 deletions security/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
<https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy>`
"""

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
73 changes: 40 additions & 33 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand All @@ -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="[email protected]",
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="[email protected]",
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},
)
1 change: 1 addition & 0 deletions testing/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
'security.middleware.MandatoryPasswordChangeMiddleware',
'security.middleware.NoConfidentialCachingMiddleware',
'security.auth_throttling.Middleware',
'security.middleware.ReferrerPolicyMiddleware',
)

ROOT_URLCONF = 'testing.urls'
Expand Down
84 changes: 65 additions & 19 deletions testing/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
)