From: Sofia Papagiannaki Date: Fri, 16 Nov 2012 18:17:43 +0000 (+0200) Subject: Restrict next url parameter X-Git-Url: https://code.grnet.gr/git/astakos/commitdiff_plain/217994f8661305ac61e040041a84cd246f9765bd Restrict next url parameter Refs: #3008 --- diff --git a/snf-astakos-app/astakos/im/target/redirect.py b/snf-astakos-app/astakos/im/target/redirect.py index 08140bb..82ae1db 100644 --- a/snf-astakos-app/astakos/im/target/redirect.py +++ b/snf-astakos-app/astakos/im/target/redirect.py @@ -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 == '': diff --git a/snf-astakos-app/astakos/im/util.py b/snf-astakos-app/astakos/im/util.py index 926ec3c..eb9d759 100644 --- a/snf-astakos-app/astakos/im/util.py +++ b/snf-astakos-app/astakos/im/util.py @@ -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: diff --git a/snf-astakos-app/astakos/im/views.py b/snf-astakos-app/astakos/im/views.py index 86e54a1..eeb02b7 100644 --- a/snf-astakos-app/astakos/im/views.py +++ b/snf-astakos-app/astakos/im/views.py @@ -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 = _('

Profile has been updated successfully

') @@ -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)