Skip to content

Commit

Permalink
Django 2 Compatible Changes (#83)
Browse files Browse the repository at this point in the history
Made the necessary changes to ensure the code is Django 2 compatible, as well marking certain middleware to be deprecated in future releases.
  • Loading branch information
tvle236 authored Mar 24, 2020
1 parent 3eddf4a commit 25073d9
Show file tree
Hide file tree
Showing 14 changed files with 250 additions and 181 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ _build/
# ignore coverage files
.coverage
htmlcov/

# development tools
.vscode/
16 changes: 4 additions & 12 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
language: python
matrix:
include:
- python: 2.7
env: DJANGO=1.11 TOXENV=pep8
- python: 2.7
env: DJANGO=1.8 TOXENV=py27-django18
- python: 2.7
env: DJANGO=1.9 TOXENV=py27-django19
- python: 2.7
env: DJANGO=1.10 TOXENV=py27-django110
- python: 2.7
env: DJANGO=1.11 TOXENV=py27-django111
- python: 3.6
env: DJANGO=1.11 TOXENV=py36-django111
- python: 2.7
env: DJANGO=1.11 TOXENV=docs
- python: 3.6
env: DJANGO=2.2 TOXENV=py36-django22
- python: 3.6
env: DJANGO=3.0 TOXENV=py36-django30

install:
- pip install tox
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ Automatically generated documentation of `django-security` is available on Read

# Requirements

* Python >= 2.7
* Django >= 1.8
* Python >= 3.6
* Django >= 1.11

For Django < 1.8 use django-security==0.9.4.
For Django < 1.8 use django-security==0.9.4. For Django < 1.11 use django-security==0.11.3.

Note: For versions prior to 0.10.0, `datetime` objects were being added to the session and required Django's PickleSerializer for (de)serializing. This has now been changed so that the strings of these `datetime`s are being stored instead. If you are still using PickleSerializer for this reason, we suggest switching to Django's default JSONSerializer (default since Django 1.6) for better security.

Expand Down Expand Up @@ -86,7 +86,7 @@ or minimum configuration.

<tr>
<td><a href="http://django-security.readthedocs.org/en/latest/#security.middleware.ContentNoSniff">ContentNoSniff</a>
<td>Disable possibly insecure autodetection of MIME types in browsers. <em>Recommended.</em>
<td><b>DEPRECATED: </b>Will be removed in future releases, consider <a href="https://docs.djangoproject.com/en/1.11/ref/middleware/#django.middleware.security.SecurityMiddleware">django.middleware.security.SecurityMiddleware</a> via <i>SECURE_CONTENT_TYPE_NOSNIFF</i> setting.<br/>Disable possibly insecure autodetection of MIME types in browsers. <em>Recommended.</em>
<td>None.

<tr>
Expand Down Expand Up @@ -116,7 +116,7 @@ or minimum configuration.

<tr>
<td><a href="http://django-security.readthedocs.org/en/latest/#security.middleware.P3PPolicyMiddleware">P3PPolicyMiddleware</a>
<td>Adds the HTTP header attribute specifying compact P3P policy.
<td><b>DEPRECATED: </b>Will be removed in future releases.<br/>Adds the HTTP header attribute specifying compact P3P policy.
<td>Required.

<tr>
Expand All @@ -126,7 +126,7 @@ or minimum configuration.

<tr>
<td><a href="http://django-security.readthedocs.org/en/latest/#security.middleware.StrictTransportSecurityMiddleware">StrictTransportSecurityMiddleware</a>
<td>Enforce SSL/TLS connection and disable plaintext fall-back. <em>Recommended</em> for SSL/TLS sites.
<td><b>DEPRECATED: </b>Will be removed in future releases, consider <a href="https://docs.djangoproject.com/en/1.11/ref/middleware/#django.middleware.security.SecurityMiddleware">django.middleware.security.SecurityMiddleware</a> via <i>SECURE_HSTS_SECONDS</i>, <i>SECURE_HSTS_INCLUDE_SUBDOMAINS</i> and <i>SECURE_HSTS_PRELOAD</i> settings.<br/>Enforce SSL/TLS connection and disable plaintext fall-back. <em>Recommended</em> for SSL/TLS sites.
<td>Optional.

<tr>
Expand All @@ -136,7 +136,7 @@ or minimum configuration.

<tr>
<td><a href="http://django-security.readthedocs.org/en/latest/#security.middleware.XssProtectMiddleware">XssProtectMiddleware</a>
<td>Enforce browser's Cross Site Scripting protection. <em>Recommended.</em>
<td><b>DEPRECATED: </b>Will be removed in future releases, consider <a href="https://docs.djangoproject.com/en/1.11/ref/middleware/#django.middleware.security.SecurityMiddleware">django.middleware.security.SecurityMiddleware</a> via <i>SECURE_BROWSER_XSS_FILTER</i> setting.<br/>Enforce browser's Cross Site Scripting protection. <em>Recommended.</em>
<td>None.

</table>
Expand Down
2 changes: 1 addition & 1 deletion requirements
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
django>=1.8,<=1.11
django>=1.11
ua_parser==0.7.1
python-dateutil==2.8.0
2 changes: 1 addition & 1 deletion security/auth_throttling/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def register_authentication_attempt(request):
"""
username = _extract_username(request)
ip = request.META["REMOTE_ADDR"]
if request.user.is_authenticated():
if request.user.is_authenticated:
reset_counters(username=username, ip=ip)
else:
increment_counters(username=username, ip=ip)
Expand Down
121 changes: 88 additions & 33 deletions security/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,27 @@
import importlib
import json
import logging
import warnings
from re import compile

import django.conf
from django.contrib.auth import logout
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse, resolve
from django.urls import reverse, resolve
from django.http import HttpResponseRedirect, HttpResponse
from django.test.signals import setting_changed
from django.utils import timezone
from django.utils.deprecation import MiddlewareMixin
import django.views.static

from ua_parser.user_agent_parser import ParseUserAgent


logger = logging.getLogger(__name__)

try:
from django.utils.deprecation import MiddlewareMixin
except ImportError:
MiddlewareMixin = object
DJANGO_SECURITY_MIDDLEWARE_URL = ("https://docs.djangoproject.com/en/1.11/ref"
"/middleware/#django.middleware.security.SecurityMiddleware")
DJANGO_CLICKJACKING_MIDDLEWARE_URL = ("https://docs.djangoproject.com/en/1.11/"
"ref/clickjacking/")


class CustomLogoutMixin(object):
Expand All @@ -46,8 +47,7 @@ def perform_logout(self, request):
return

try:
module = self.CUSTOM_LOGOUT_MODULE
module_path, function_name = module.rsplit('.', 1)
module_path, function_name = self.CUSTOM_LOGOUT_MODULE.rsplit('.', 1)
except ValueError:
err = self.Messages.NOT_A_MODULE_PATH
raise Exception(err.format(self.CUSTOM_LOGOUT_MODULE))
Expand Down Expand Up @@ -167,13 +167,9 @@ def process_request(self, request):
"""
Read DNT header from browser request and create request attribute
"""
request.dnt = None
if 'HTTP_DNT' in request.META:
if request.META['HTTP_DNT'] == '1':
request.dnt = True
else:
request.dnt = False
else:
request.dnt = None
request.dnt = request.META['HTTP_DNT'] == '1'
# returns None in normal conditions

def process_response(self, request, response):
Expand All @@ -188,6 +184,10 @@ def process_response(self, request, response):

class XssProtectMiddleware(BaseMiddleware):
"""
DEPRECATED: Will be removed in future releases. Consider
django.middleware.security.SecurityMiddleware as a replacement for this via
SECURE_BROWSER_XSS_FILTER setting.
Sends X-XSS-Protection HTTP header that controls Cross-Site Scripting
filter on MSIE. Use XSS_PROTECT option in settings file with the following
values:
Expand Down Expand Up @@ -223,6 +223,15 @@ class XssProtectMiddleware(BaseMiddleware):

DEFAULT = 'sanitize'

def __init__(self, get_response=None):
super().__init__(get_response)
warnings.warn('DEPRECATED: The middleware "{name}" will no longer be '
'supported in future releases of this library. Refer to {url} for an '
'alternative approach with regards to the settings: {settings}'.format(
name=self.__class__.__name__,
url=DJANGO_SECURITY_MIDDLEWARE_URL,
settings="SECURE_BROWSER_XSS_FILTER"))

def load_setting(self, setting, value):
if not value:
self.option = self.DEFAULT
Expand Down Expand Up @@ -305,6 +314,10 @@ def process_response(self, request, response):

class ContentNoSniff(MiddlewareMixin):
"""
DEPRECATED: Will be removed in future releases. Consider
django.middleware.security.SecurityMiddleware as a replacement for this via
SECURE_CONTENT_TYPE_NOSNIFF setting.
Sends X-Content-Options HTTP header to disable autodetection of MIME type
of files returned by the server in Microsoft Internet Explorer.
Specifically if this flag is enabled, MSIE will not load external CSS and
Expand All @@ -322,6 +335,16 @@ class ContentNoSniff(MiddlewareMixin):
<http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx>`_
"""

def __init__(self, get_response=None):
super().__init__(get_response)
warnings.warn('DEPRECATED: The middleware "{name}" will no longer be '
'supported in future releases of this library. Refer to {url} for an '
'alternative approach with regards to the settings: {settings}'.format(
name=self.__class__.__name__,
url=DJANGO_SECURITY_MIDDLEWARE_URL,
settings="SECURE_CONTENT_TYPE_NOSNIFF"))


def process_response(self, request, response):
"""
Add ``X-Content-Options: nosniff`` to the response header.
Expand Down Expand Up @@ -365,7 +388,7 @@ def process_view(self, request, view, *args, **kwargs):
if not self.settings:
return

if not request.user.is_authenticated():
if not request.user.is_authenticated:
return

if view == django.views.static.serve:
Expand Down Expand Up @@ -446,25 +469,25 @@ def process_response(self, request, response):
whitelist non-confidential pages and treat all others as non-
confidential, or specifically blacklist pages as confidential
"""

def match(path, match_list):
path = path.lstrip('/')
return any(re.match(path) for re in match_list)

def remove_response_caching(response):
response['Cache-control'] = \
'no-cache, no-store, max-age=0, must-revalidate'
response['Pragma'] = "no-cache"
response['Expires'] = -1

path = request.path.lstrip('/')
if self.whitelist:
if not match(request.path, self.whitelist_url_regexes):
remove_response_caching(response)
if not any(re.match(path) for re in self.whitelist_url_regexes):
self._remove_response_caching(response)
return response
if self.blacklist:
if match(request.path, self.blacklist_url_regexes):
remove_response_caching(response)
if any(re.match(path) for re in self.blacklist_url_regexes):
self._remove_response_caching(response)
return response

def _remove_response_caching(self, response):
"""
Overwrites specific headers to make the HTTP response confidential.
"""
response['Cache-control'] = \
'no-cache, no-store, max-age=0, must-revalidate'
response['Pragma'] = "no-cache"
response['Expires'] = -1


# http://tools.ietf.org/html/draft-ietf-websec-x-frame-options-01
# http://tools.ietf.org/html/draft-ietf-websec-frame-options-00
Expand Down Expand Up @@ -505,6 +528,13 @@ class XFrameOptionsMiddleware(BaseMiddleware):

DEFAULT = 'deny'

def __init__(self, get_response=None):
super().__init__(get_response)
warnings.warn('An official middleware "{name}" is supported by Django.'
' Refer to {url} to see if its approach fits the use case.'.format(
name="XFrameOptionsMiddleware",
url=DJANGO_CLICKJACKING_MIDDLEWARE_URL))

def load_setting(self, setting, value):
if setting == 'X_FRAME_OPTIONS':
if not value:
Expand Down Expand Up @@ -836,6 +866,11 @@ def process_response(self, request, response):

class StrictTransportSecurityMiddleware(MiddlewareMixin):
"""
DEPRECATED: Will be removed in future releases. Consider
django.middleware.security.SecurityMiddleware as a replacement for this via
SECURE_HSTS_SECONDS, SECURE_HSTS_INCLUDE_SUBDOMAINS and
SECURE_HSTS_PRELOAD settings.
Adds Strict-Transport-Security header to HTTP
response that enforces SSL connections on compliant browsers. Two
parameters can be set in settings file, otherwise reasonable
Expand All @@ -859,6 +894,17 @@ class StrictTransportSecurityMiddleware(MiddlewareMixin):
- `Preloaded HSTS sites <http://www.chromium.org/sts>`_
"""
def __init__(self, get_response=None):
warnings.warn('DEPRECATED: The middleware "{name}" will no longer be '
'supported in future releases of this library. Refer to {url} for an '
'alternative approach with regards to the settings: {settings}'.format(
name=self.__class__.__name__,
url=DJANGO_SECURITY_MIDDLEWARE_URL,
settings=", ".join([
"SECURE_HSTS_SECONDS",
"SECURE_HSTS_INCLUDE_SUBDOMAINS",
"SECURE_HSTS_PRELOAD",
])))

self.get_response = get_response

try:
Expand Down Expand Up @@ -894,6 +940,8 @@ def process_response(self, request, response):

class P3PPolicyMiddleware(BaseMiddleware):
"""
DEPRECATED: Will be removed in future releases.
Adds the HTTP header attribute specifying compact P3P policy
defined in P3P_COMPACT_POLICY setting and location of full
policy defined in P3P_POLICY_URL. If the latter is not defined,
Expand All @@ -912,6 +960,13 @@ class P3PPolicyMiddleware(BaseMiddleware):
REQUIRED_SETTINGS = ("P3P_COMPACT_POLICY",)
OPTIONAL_SETTINGS = ("P3P_POLICY_URL",)

def __init__(self, get_response=None):
super().__init__(get_response)
warnings.warn('DEPRECATED: The middleware "{name}" will no longer be '
'supported in future releases of this library.'.format(
name=self.__class__.__name__
))

def load_setting(self, setting, value):
if setting == 'P3P_COMPACT_POLICY':
self.policy = value
Expand Down Expand Up @@ -1082,7 +1137,7 @@ def process_existing_session(self, request):
logger.debug("Session %s is inactive.", session.session_key)
response = None

if request.user.is_authenticated():
if request.user.is_authenticated:
# Store the current path in the session
# so we can redirect the user after the logout
response = self.perform_logout(request)
Expand Down Expand Up @@ -1169,13 +1224,13 @@ def assert_authentication_middleware_installed(self, request):
def process_request(self, request):
self.assert_authentication_middleware_installed(request)

if request.user.is_authenticated() and not request.user.is_active:
if request.user.is_authenticated and not request.user.is_active:
response = self.perform_logout(request)

if response:
return response

if request.user.is_authenticated():
if request.user.is_authenticated:
return

path = request.path_info.lstrip('/')
Expand Down
2 changes: 1 addition & 1 deletion security/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('password_expiry_date', models.DateTimeField(help_text="The date and time when the user's password expires. If this is empty, the password never expires.", auto_now_add=True, null=True)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, unique=True)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, unique=True, on_delete=models.CASCADE)),
],
options={
'verbose_name_plural': 'PasswordExpiries',
Expand Down
6 changes: 3 additions & 3 deletions security/south_migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def forwards(self, orm):
# Adding model 'PasswordExpiry'
db.create_table('security_passwordexpiry', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], unique=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], unique=True, on_delete=models.CASCADE)),
('password_expiry_date', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(1, 1, 1, 0, 0))),
))
db.send_create_signal('security', ['PasswordExpiry'])
Expand All @@ -33,7 +33,7 @@ def backwards(self, orm):
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'on_delete': 'django.db.models.CASCADE'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
Expand Down Expand Up @@ -64,7 +64,7 @@ def backwards(self, orm):
'Meta': {'object_name': 'PasswordExpiry'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'password_expiry_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 2, 0, 0)'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True', 'on_delete': 'django.db.models.CASCADE'})
}
}

Expand Down
Loading

0 comments on commit 25073d9

Please sign in to comment.