Require explicit tagging on views taking query parameters
authorMagnus Hagander <magnus@hagander.net>
Mon, 22 Feb 2021 09:43:59 +0000 (10:43 +0100)
committerMagnus Hagander <magnus@hagander.net>
Mon, 22 Feb 2021 09:43:59 +0000 (10:43 +0100)
Require each view to declare which query parameters it wants, and filter
out any other parameters.

We have very few views that actually take query parameters, and random
additional query patterns will have no effect on the view. However, they
will break frontend caching (in making them look like different pages).

This will be extended into an implementation in the caching frontends as
well, btu it's needed in the backend to ensure that local testing will
have tbe same effect as the caches.

pgweb/account/views.py
pgweb/search/views.py
pgweb/util/decorators.py
pgweb/util/middleware.py

index 1f723bc4f4a677e24578ece37e2e284e7c0e3ce6..ae42851c3e973f76557b5dfd8139ab7155d8248e 100644 (file)
@@ -4,7 +4,7 @@ import django.contrib.auth.views as authviews
 from django.http import HttpResponseRedirect, Http404, HttpResponse
 from django.core.exceptions import PermissionDenied
 from django.shortcuts import get_object_or_404
-from pgweb.util.decorators import login_required, script_sources, frame_sources, content_sources
+from pgweb.util.decorators import login_required, script_sources, frame_sources, content_sources, queryparams
 from django.views.decorators.csrf import csrf_exempt
 from django.utils.encoding import force_bytes
 from django.utils.http import urlsafe_base64_encode
@@ -535,6 +535,7 @@ def signup_complete(request):
 @script_sources('https://www.gstatic.com/recaptcha/')
 @frame_sources('https://www.google.com/')
 @transaction.atomic
+@queryparams('do_abort')
 def signup_oauth(request):
     if 'oauth_email' not in request.session \
        or 'oauth_firstname' not in request.session \
@@ -618,6 +619,7 @@ def signup_oauth(request):
 ####
 # Community authentication endpoint
 ####
+@queryparams('d', 'su')
 def communityauth(request, siteid):
     # Get whatever site the user is trying to log in to.
     site = get_object_or_404(CommunityAuthSite, pk=siteid)
@@ -742,6 +744,7 @@ def communityauth_logout(request, siteid):
 
 
 @login_required
+@queryparams('next')
 def communityauth_consent(request, siteid):
     org = get_object_or_404(CommunityAuthSite, id=siteid).org
     if request.method == 'POST':
@@ -776,6 +779,7 @@ def _encrypt_site_response(site, s):
     )
 
 
+@queryparams('s', 'e', 'n', 'u')
 def communityauth_search(request, siteid):
     # Perform a search for users. The response will be encrypted with the site
     # key to prevent abuse, therefor we need the site.
index 049916fd3b07506b97b21bdbb53efecd40e583c7..cfed3651732f492b03668d69d3a34870672d85cd 100644 (file)
@@ -3,7 +3,7 @@ from django.http import HttpResponseRedirect, Http404
 from django.views.decorators.csrf import csrf_exempt
 from django.conf import settings
 
-from pgweb.util.decorators import cache
+from pgweb.util.decorators import cache, queryparams
 
 import urllib.parse
 import requests
@@ -47,6 +47,7 @@ def generate_pagelinks(pagenum, totalpages, querystring):
 
 
 @csrf_exempt
+@queryparams('a', 'd', 'l', 'ln', 'm', 'p', 'q', 's', 'u')
 @cache(minutes=30)
 def search(request):
     # Perform a general web search
index 9333d456f55f1a4026c65cfafdc252f092c6a714..c893973c3427b938649ce27f1f195795674ef12b 100644 (file)
@@ -24,6 +24,17 @@ def cache(days=0, hours=0, minutes=0, seconds=0):
     return _cache
 
 
+def queryparams(*args):
+    """
+    Allow specified query parameters when calling function.
+    NOTE! Must be the "outermost" decorator!!!
+    """
+    def _queryparams(fn):
+        fn.queryparams = args
+        return fn
+    return _queryparams
+
+
 def allow_frames(fn):
     def _allow_frames(request, *_args, **_kwargs):
         resp = fn(request, *_args, **_kwargs)
index 1609e00174a248d44ae7eb395ce98429bc033ce6..6a6e3dbf67cd2886ce1149a474141edd0fd77d95 100644 (file)
@@ -1,4 +1,5 @@
 from django.conf import settings
+from django.http import QueryDict
 
 from pgweb.util.templateloader import initialize_template_collection, get_all_templates
 
@@ -76,3 +77,29 @@ class PgMiddleware(object):
 
         response['X-XSS-Protection'] = "1; mode=block"
         return response
+
+    def process_view(self, request, view_func, view_args, view_kwargs):
+        # Filter out any query parameters that are not explicitly allowed. We do the same thing in Varnish,
+        # and it's better to also do it in django if they show up here, so issues because of it are caught
+        # in local testing where there is no Varnish.
+        if not request.GET:
+            # If there are no parameters, just skip this whole process
+            return None
+
+        if request.path.startswith('/admin/'):
+            # django-admin uses it a lot and it's not for us to change
+            return None
+
+        allowed = getattr(view_func, 'queryparams', None)
+
+        if allowed:
+            # Filter the QueryDict for only the allowed parameters
+            result = request.GET.copy()
+            for k in request.GET.keys():
+                if k not in allowed:
+                    del result[k]
+            result.mutable = False
+            request.GET = result
+        else:
+            request.GET = QueryDict()
+        return None