mirror of
https://github.com/postgres/pgweb.git
synced 2025-08-13 13:12:42 +00:00
Enable setting of security http headers
The following security policy headers are set: X-XSS-Protection: 1; mode=block -- always set X-Frame-Options: DENY is set for all pages except for the documentation pages, primarily because pgadmin4 loads them in an iframe which would break. Content-Security-Policy: <x>-src Is set to allow the default of self only, then allowing scripts for google analytics and fonts for google fonts. Images are allowed from everywhere. frame-ancestors 'none' is set by the same rules as X-Frame-Options This also adds a decorator for @script_sources to have a single view allow extra sources, and this is used for recaptcha. A generic decorator is also made for other types of exclusions, though we don't have any at this point. If the setting SECURITY_POLICY_REPORT_ONLY is set to True then the policy will be report-only and not enforced (for testing), otherwise enforcing mode is enabled. The setting SECURITY_POLICY_REPORT_URI sets where to send security policy reports, if any.
This commit is contained in:
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user