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
|
import django.contrib.auth.views as authviews
|
||||||
from django.http import HttpResponseRedirect, Http404, HttpResponse
|
from django.http import HttpResponseRedirect, Http404, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
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.encoding import force_bytes
|
||||||
from django.utils.http import urlsafe_base64_encode
|
from django.utils.http import urlsafe_base64_encode
|
||||||
from django.contrib.auth.tokens import default_token_generator
|
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)))
|
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')
|
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):
|
def signup(request):
|
||||||
if request.user.is_authenticated():
|
if request.user.is_authenticated():
|
||||||
return HttpServerError(request, "You must log out before you can sign up for a new account")
|
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.shortcuts import render, get_object_or_404
|
||||||
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
|
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
|
||||||
from django.http import Http404
|
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.db.models import Q
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
@ -17,6 +17,7 @@ from pgweb.core.models import Version
|
|||||||
from models import DocPage
|
from models import DocPage
|
||||||
from forms import DocCommentForm
|
from forms import DocCommentForm
|
||||||
|
|
||||||
|
@allow_frames
|
||||||
def docpage(request, version, filename):
|
def docpage(request, version, filename):
|
||||||
loaddate = None
|
loaddate = None
|
||||||
# Get the current version both to map the /current/ url, and to later
|
# Get the current version both to map the /current/ url, and to later
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from collections import defaultdict
|
||||||
from django.contrib.auth.decorators import login_required as django_login_required
|
from django.contrib.auth.decorators import login_required as django_login_required
|
||||||
|
|
||||||
def nocache(fn):
|
def nocache(fn):
|
||||||
@ -20,6 +21,27 @@ def cache(days=0, hours=0, minutes=0, seconds=0):
|
|||||||
return __cache
|
return __cache
|
||||||
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
|
# A wrapped version of login_required that throws an exception if it's
|
||||||
# used on a path that's not under /account/.
|
# used on a path that's not under /account/.
|
||||||
def login_required(f):
|
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 pgweb.util.templateloader import initialize_template_collection, get_all_templates
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
# Use thread local storage to pass the username down.
|
# Use thread local storage to pass the username down.
|
||||||
@ -26,10 +29,42 @@ class PgMiddleware(object):
|
|||||||
initialize_template_collection()
|
initialize_template_collection()
|
||||||
|
|
||||||
def process_response(self, request, response):
|
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()
|
tlist = get_all_templates()
|
||||||
if 'base/esi.html' in tlist:
|
if 'base/esi.html' in tlist:
|
||||||
response['x-do-esi'] = "1"
|
response['x-do-esi'] = "1"
|
||||||
tlist.remove('base/esi.html')
|
tlist.remove('base/esi.html')
|
||||||
if tlist:
|
if tlist:
|
||||||
response['xkey'] = ' '.join(["pgwt_{0}".format(hashlib.md5(t).hexdigest()) for t in 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
|
return response
|
||||||
|
Reference in New Issue
Block a user