Restrict next url parameter
authorSofia Papagiannaki <papagian@gmail.com>
Fri, 16 Nov 2012 18:17:43 +0000 (20:17 +0200)
committerSofia Papagiannaki <papagian@gmail.com>
Fri, 16 Nov 2012 18:17:43 +0000 (20:17 +0200)
Refs: #3008

snf-astakos-app/astakos/im/target/redirect.py
snf-astakos-app/astakos/im/util.py
snf-astakos-app/astakos/im/views.py

index 08140bb..82ae1db 100644 (file)
@@ -37,7 +37,9 @@ from django.utils.translation import ugettext as _
 from django.contrib import messages
 from django.utils.http import urlencode
 from django.contrib.auth import authenticate
-from django.http import HttpResponse, HttpResponseBadRequest
+from django.http import (
+    HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
+)
 from django.core.exceptions import ValidationError
 from django.views.decorators.http import require_http_methods
 
@@ -45,7 +47,7 @@ from urllib import quote
 from urlparse import urlunsplit, urlsplit, urlparse, parse_qsl
 
 from astakos.im.settings import COOKIE_NAME, COOKIE_DOMAIN
-from astakos.im.util import set_cookie
+from astakos.im.util import set_cookie, restrict_next
 from astakos.im.functions import login as auth_login, logout
 
 import logging
@@ -65,6 +67,10 @@ def login(request):
     next = request.GET.get('next')
     if not next:
         return HttpResponseBadRequest(_('No next parameter'))
+    if not restrict_next(
+        next, domain=COOKIE_DOMAIN, allowed_schemes=('pithos',)
+    ):
+        return HttpResponseForbidden(_('Not allowed next parameter'))
     force = request.GET.get('force', None)
     response = HttpResponse()
     if force == '':
index 926ec3c..eb9d759 100644 (file)
@@ -36,7 +36,7 @@ import datetime
 import time
 
 from urllib import quote
-from urlparse import urlsplit, urlunsplit
+from urlparse import urlsplit, urlunsplit, urlparse
 
 from datetime import tzinfo, timedelta
 from django.http import HttpResponse, HttpResponseBadRequest, urlencode
@@ -47,8 +47,10 @@ from django.core.urlresolvers import reverse
 from django.core.exceptions import ValidationError
 
 from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
-from astakos.im.settings import INVITATIONS_PER_LEVEL, COOKIE_NAME, \
-    COOKIE_DOMAIN, COOKIE_SECURE, FORCE_PROFILE_UPDATE, LOGGING_LEVEL
+from astakos.im.settings import (
+    INVITATIONS_PER_LEVEL, COOKIE_NAME, COOKIE_DOMAIN, COOKIE_SECURE,
+    FORCE_PROFILE_UPDATE, LOGGING_LEVEL
+)
 from astakos.im.functions import login
 
 logger = logging.getLogger(__name__)
@@ -96,6 +98,51 @@ def get_invitation(request):
         raise ValueError(_('Email: %s is reserved' % invitation.username))
     return invitation
 
+def restrict_next(url, domain=None, allowed_schemes=()):
+    """
+    Return url if having the supplied ``domain`` (if present) or one of the ``allowed_schemes``.
+    Otherwise return None.
+    
+    >>> print restrict_next('/im/feedback', '.okeanos.grnet.gr')
+    /im/feedback
+    >>> print restrict_next('pithos.okeanos.grnet.gr/im/feedback', '.okeanos.grnet.gr')
+    pithos.okeanos.grnet.gr/im/feedback
+    >>> print restrict_next('https://pithos.okeanos.grnet.gr/im/feedback', '.okeanos.grnet.gr')
+    https://pithos.okeanos.grnet.gr/im/feedback
+    >>> print restrict_next('pithos://127.0.0,1', '.okeanos.grnet.gr')
+    None
+    >>> print restrict_next('pithos://127.0.0,1', '.okeanos.grnet.gr', allowed_schemes=('pithos'))
+    pithos://127.0.0,1
+    >>> print restrict_next('node1.example.com', '.okeanos.grnet.gr')
+    None
+    >>> print restrict_next('//node1.example.com', '.okeanos.grnet.gr')
+    None
+    >>> print restrict_next('https://node1.example.com', '.okeanos.grnet.gr')
+    None
+    >>> print restrict_next('https://node1.example.com')
+    https://node1.example.com
+    >>> print restrict_next('//node1.example.com')
+    //node1.example.com
+    >>> print restrict_next('node1.example.com')
+    node1.example.com
+    """
+    if not url:
+        return
+    parts = urlparse(url, scheme='http')
+    if not parts.netloc:
+        # fix url if does not conforms RFC 1808
+        url = '//%s' % url
+        parts = urlparse(url, scheme='http')
+    # TODO more scientific checks?
+    if not parts.netloc:    # internal url
+        return url
+    elif not domain:
+        return url
+    elif parts.netloc.endswith(domain):
+        return url
+    elif parts.scheme in allowed_schemes:
+        return url
+
 def prepare_response(request, user, next='', renew=False):
     """Return the unique username and the token
        as 'X-Auth-User' and 'X-Auth-Token' headers,
@@ -115,6 +162,8 @@ def prepare_response(request, user, next='', renew=False):
         except ValidationError, e:
             return HttpResponseBadRequest(e) 
     
+    next = restrict_next(next, domain=COOKIE_DOMAIN)
+    
     if FORCE_PROFILE_UPDATE and not user.is_verified and not user.is_superuser:
         params = ''
         if next:
index 86e54a1..eeb02b7 100644 (file)
@@ -56,7 +56,9 @@ from django.views.decorators.http import require_http_methods
 
 from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
 from astakos.im.activation_backends import get_backend, SimpleBackend
-from astakos.im.util import get_context, prepare_response, set_cookie, get_query
+from astakos.im.util import (
+    get_context, prepare_response, set_cookie, get_query, restrict_next
+)
 from astakos.im.forms import *
 from astakos.im.functions import (send_greeting, send_feedback, SendMailError,
     invite as invite_func, logout as auth_logout, activate as activate_func
@@ -268,7 +270,10 @@ def edit_profile(request, template_name='im/profile.html', extra_context={}):
                 user = form.save()
                 reset_cookie = user.auth_token != prev_token
                 form = ProfileForm(instance=user)
-                next = request.POST.get('next')
+                next = restrict_next(
+                    request.POST.get('next'),
+                    domain=COOKIE_DOMAIN
+                )
                 if next:
                     return redirect(next)
                 msg = _('<p>Profile has been updated successfully</p>')
@@ -419,7 +424,7 @@ def feedback(request, template_name='im/feedback.html', email_template_name='im/
                            feedback_form = form,
                            context_instance = get_context(request, extra_context))
 
-@require_http_methods(["GET", "POST"])
+@require_http_methods(["GET"])
 def logout(request, template='registration/logged_out.html', extra_context={}):
     """
     Wraps `django.contrib.auth.logout` and delete the cookie.
@@ -431,7 +436,10 @@ def logout(request, template='registration/logged_out.html', extra_context={}):
         response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
         msg = 'Cookie deleted for %s' % email
         logger._log(LOGGING_LEVEL, msg, [])
-    next = request.GET.get('next')
+    next = restrict_next(
+        request.GET.get('next'),
+        domain=COOKIE_DOMAIN
+    )
     if next:
         response['Location'] = next
         response.status_code = 302
@@ -506,7 +514,10 @@ def approval_terms(request, term_id=None, template_name='im/approval_terms.html'
     terms = f.read()
 
     if request.method == 'POST':
-        next = request.POST.get('next')
+        next = restrict_next(
+            request.POST.get('next'),
+            domain=COOKIE_DOMAIN
+        )
         if not next:
             next = reverse('astakos.im.views.index')
         form = SignApprovalTermsForm(request.POST, instance=request.user)