diff --git a/pgweb/account/views.py b/pgweb/account/views.py index 88df6245..61210525 100644 --- a/pgweb/account/views.py +++ b/pgweb/account/views.py @@ -3,7 +3,7 @@ from django.contrib.auth import login as django_login import django.contrib.auth.views as authviews from django.http import HttpResponseRedirect, Http404, HttpResponse from django.shortcuts import get_object_or_404 -from pgweb.util.decorators import login_required +from pgweb.util.decorators import login_required, script_sources from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode from django.contrib.auth.tokens import default_token_generator @@ -293,6 +293,8 @@ def reset_complete(request): log.info("Password reset completed for user from {0}".format(get_client_ip(request))) return authviews.password_reset_complete(request, template_name='account/password_reset_complete.html') +@script_sources('https://www.google.com/recaptcha/') +@script_sources('https://www.gstatic.com/recaptcha/') def signup(request): if request.user.is_authenticated(): return HttpServerError(request, "You must log out before you can sign up for a new account") diff --git a/pgweb/docs/views.py b/pgweb/docs/views.py index b800a866..b99adf59 100644 --- a/pgweb/docs/views.py +++ b/pgweb/docs/views.py @@ -1,7 +1,7 @@ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect from django.http import Http404 -from pgweb.util.decorators import login_required +from pgweb.util.decorators import login_required, allow_frames from django.db.models import Q from django.conf import settings @@ -17,6 +17,7 @@ from pgweb.core.models import Version from models import DocPage from forms import DocCommentForm +@allow_frames def docpage(request, version, filename): loaddate = None # Get the current version both to map the /current/ url, and to later diff --git a/pgweb/util/decorators.py b/pgweb/util/decorators.py index 3ea4109c..2af6012e 100644 --- a/pgweb/util/decorators.py +++ b/pgweb/util/decorators.py @@ -1,5 +1,6 @@ import datetime from functools import wraps +from collections import defaultdict from django.contrib.auth.decorators import login_required as django_login_required def nocache(fn): @@ -20,6 +21,27 @@ def cache(days=0, hours=0, minutes=0, seconds=0): return __cache return _cache +def allow_frames(fn): + def _allow_frames(request, *_args, **_kwargs): + resp = fn(request, *_args, **_kwargs) + resp.x_allow_frames = True + return resp + return _allow_frames + +def content_sources(what, source): + def _script_sources(fn): + def __script_sources(request, *_args, **_kwargs): + resp = fn(request, *_args, **_kwargs) + if not hasattr(resp, 'x_allow_extra_sources'): + resp.x_allow_extra_sources = defaultdict(list) + resp.x_allow_extra_sources[what].append(source) + return resp + return __script_sources + return _script_sources + +def script_sources(source): + return content_sources('script', source) + # A wrapped version of login_required that throws an exception if it's # used on a path that's not under /account/. def login_required(f): diff --git a/pgweb/util/middleware.py b/pgweb/util/middleware.py index d8d46811..e9f672ce 100644 --- a/pgweb/util/middleware.py +++ b/pgweb/util/middleware.py @@ -1,5 +1,8 @@ +from django.conf import settings + from pgweb.util.templateloader import initialize_template_collection, get_all_templates +from collections import OrderedDict import hashlib # Use thread local storage to pass the username down. @@ -26,10 +29,42 @@ class PgMiddleware(object): initialize_template_collection() def process_response(self, request, response): + # Set xkey representing the templates that are in use so we can do efficient + # varnish purging on commits. tlist = get_all_templates() if 'base/esi.html' in tlist: response['x-do-esi'] = "1" tlist.remove('base/esi.html') if tlist: response['xkey'] = ' '.join(["pgwt_{0}".format(hashlib.md5(t).hexdigest()) for t in tlist]) + + # Set security headers + sources = OrderedDict([ + ('default', ["'self'", ]), + ('img', ['*', ]), + ('script', ["'self'", "www.google-analytics.com"]), + ('media', ["'self'", ]), + ('style', ["'self'", "fonts.googleapis.com"]), + ('font', ["'self'", "fonts.gstatic.com"]), + ]) + if hasattr(response, 'x_allow_extra_sources'): + for k,v in response.x_allow_extra_sources.items(): + sources[k].extend(v) + + security_policies = ["{0}-src {1}".format(k," ".join(v)) for k,v in sources.items()] + + if not getattr(response, 'x_allow_frames', False): + response['X-Frame-Options'] = 'DENY' + security_policies.append("frame-ancestors 'none'") + + if hasattr(settings, 'SECURITY_POLICY_REPORT_URI'): + security_policies.append("report-uri " + settings.SECURITY_POLICY_REPORT_URI) + + if security_policies: + if getattr(settings, 'SECURITY_POLICY_REPORT_ONLY', False): + response['Content-Security-Policy-Report-Only'] = " ; ".join(security_policies) + else: + response['Content-Security-Policy'] = " ; ".join(security_policies) + + response['X-XSS-Protection'] = "1; mode=block" return response