def _is_safe_redirect(req, url): return is_safe_login_or_logout_redirect( redirect_to=url, request_host=req.get_host(), dot_client_id=req.GET.get('client_id'), require_https=req.is_secure(), )
def get(self, request): import base64 from openedx.core.djangoapps.user_authn.utils import is_safe_login_or_logout_redirect ticket = request.GET.get('ticket') redirect_url = base64.b64decode( request.GET.get( 'next', "Lw==")).decode('utf-8') if not is_safe_login_or_logout_redirect(redirect_url, request.get_host(), None, False): redirect_url = "/" error_url = reverse('uchileedxlogin-login:login') if ticket is None: logger.exception("error ticket") return HttpResponseRedirect( '{}?next={}'.format( error_url, redirect_url)) username = self.verify_state(request, ticket) if username is None: logger.exception("Error username ") return HttpResponseRedirect( '{}?next={}'.format( error_url, redirect_url)) try: self.login_user(request, username) except Exception: logger.exception("Error logging " + username + " - " + ticket) return HttpResponseRedirect( '{}?next={}'.format( error_url, redirect_url)) return HttpResponseRedirect(redirect_url)
def target(self): """ If a redirect_url is specified in the querystring for this request, and the value is a safe url for redirect, the view will redirect to this page after rendering the template. If it is not specified, we will use the default target url. """ target_url = self.request.GET.get( 'redirect_url') or self.request.GET.get('next') # Some third party apps do not build URLs correctly and send next query param without URL-encoding, resulting # all plus('+') signs interpreted as space(' ') in the process of URL-decoding # for example if we hit on: # >> http://example.com/logout?next=/courses/course-v1:ARTS+D1+2018_T/course/ # we will receive in request.GET['next'] # >> /courses/course-v1:ARTS D1 2018_T/course/ # instead of # >> /courses/course-v1:ARTS+D1+2018_T/course/ # to handle this scenario we need to encode our URL using quote_plus and then unquote it again. if target_url: target_url = parse.unquote(parse.quote_plus(target_url)) use_target_url = target_url and is_safe_login_or_logout_redirect( redirect_to=target_url, request_host=self.request.get_host(), dot_client_id=self.request.GET.get('client_id'), require_https=self.request.is_secure(), ) return target_url if use_target_url else self.default_target
def _get_redirect_to(request): """ Determine the redirect url and return if safe :argument request: request object :returns: redirect url if safe else None """ redirect_to = request.GET.get('next') header_accept = request.META.get('HTTP_ACCEPT', '') # If we get a redirect parameter, make sure it's safe i.e. not redirecting outside our domain. # Also make sure that it is not redirecting to a static asset and redirected page is web page # not a static file. As allowing assets to be pointed to by "next" allows 3rd party sites to # get information about a user on edx.org. In any such case drop the parameter. if redirect_to: mime_type, _ = mimetypes.guess_type(redirect_to, strict=False) if not is_safe_login_or_logout_redirect(request, redirect_to): log.warning( u"Unsafe redirect parameter detected after login page: '%(redirect_to)s'", {"redirect_to": redirect_to} ) redirect_to = None elif 'text/html' not in header_accept: log.info( u"Redirect to non html content '%(content_type)s' detected from '%(user_agent)s'" u" after login page: '%(redirect_to)s'", { "redirect_to": redirect_to, "content_type": header_accept, "user_agent": request.META.get('HTTP_USER_AGENT', '') } ) redirect_to = None elif mime_type: log.warning( u"Redirect to url path with specified filed type '%(mime_type)s' not allowed: '%(redirect_to)s'", {"redirect_to": redirect_to, "mime_type": mime_type} ) redirect_to = None elif settings.STATIC_URL in redirect_to: log.warning( u"Redirect to static content detected after login page: '%(redirect_to)s'", {"redirect_to": redirect_to} ) redirect_to = None else: themes = get_themes() next_path = six.moves.urllib.parse.urlparse(redirect_to).path for theme in themes: if theme.theme_dir_name in next_path: log.warning( u"Redirect to theme content detected after login page: '%(redirect_to)s'", {"redirect_to": redirect_to} ) redirect_to = None break return redirect_to
def _get_redirect_to(request): """ Determine the redirect url and return if safe :argument request: request object :returns: redirect url if safe else None """ redirect_to = request.GET.get('next') header_accept = request.META.get('HTTP_ACCEPT', '') # If we get a redirect parameter, make sure it's safe i.e. not redirecting outside our domain. # Also make sure that it is not redirecting to a static asset and redirected page is web page # not a static file. As allowing assets to be pointed to by "next" allows 3rd party sites to # get information about a user on edx.org. In any such case drop the parameter. if redirect_to: mime_type, _ = mimetypes.guess_type(redirect_to, strict=False) if not is_safe_login_or_logout_redirect(request, redirect_to): log.warning( u'Unsafe redirect parameter detected after login page: %(redirect_to)r', {"redirect_to": redirect_to} ) redirect_to = None elif 'text/html' not in header_accept: log.info( u'Redirect to non html content %(content_type)r detected from %(user_agent)r' u' after login page: %(redirect_to)r', { "redirect_to": redirect_to, "content_type": header_accept, "user_agent": request.META.get('HTTP_USER_AGENT', '') } ) redirect_to = None elif mime_type: log.warning( u'Redirect to url path with specified filed type %(mime_type)r not allowed: %(redirect_to)r', {"redirect_to": redirect_to, "mime_type": mime_type} ) redirect_to = None elif settings.STATIC_URL in redirect_to: log.warning( u'Redirect to static content detected after login page: %(redirect_to)r', {"redirect_to": redirect_to} ) redirect_to = None else: themes = get_themes() next_path = urlparse.urlparse(redirect_to).path for theme in themes: if theme.theme_dir_name in next_path: log.warning( u'Redirect to theme content detected after login page: %(redirect_to)r', {"redirect_to": redirect_to} ) redirect_to = None break return redirect_to
def test_safe_redirect_oauth2(self, client_redirect_uri, redirect_url, host, expected_is_safe): """ Test safe redirect_url parameter when logging out OAuth2 client. """ application = ApplicationFactory(redirect_uris=client_redirect_uri) params = { 'client_id': application.client_id, 'redirect_url': redirect_url, } req = self.request.get('/logout?{}'.format(urlencode(params)), HTTP_HOST=host) actual_is_safe = is_safe_login_or_logout_redirect(req, redirect_url) self.assertEqual(actual_is_safe, expected_is_safe)
def test_safe_redirect_oauth2(self, client_redirect_uri, redirect_url, host, expected_is_safe): """ Test safe redirect_url parameter when logging out OAuth2 client. """ application = ApplicationFactory(redirect_uris=client_redirect_uri) params = { 'client_id': application.client_id, 'redirect_url': redirect_url, } req = self.request.get('/logout?{}'.format(urlencode(params)), HTTP_HOST=host) actual_is_safe = is_safe_login_or_logout_redirect(req, redirect_url) self.assertEqual(actual_is_safe, expected_is_safe)
def target(self): """ If a redirect_url is specified in the querystring for this request, and the value is a safe url for redirect, the view will redirect to this page after rendering the template. If it is not specified, we will use the default target url. """ target_url = self.request.GET.get('redirect_url') or self.request.GET.get('next') if target_url and is_safe_login_or_logout_redirect(self.request, target_url): return target_url else: return self.default_target
def ensure_redirect_url_is_safe(strategy, *args, **kwargs): """ Ensure that the redirect url is save if a user logs in or registers by directly hitting the TPA url i.e /auth/login/backend_name?next=<redirect_to> Check it against the LOGIN_REDIRECT_WHITELIST. If it is not safe then redirect to SOCIAL_AUTH_LOGIN_REDIRECT_URL (defaults to /dashboard) """ redirect_to = strategy.session_get(REDIRECT_FIELD_NAME, None) request = strategy.request if redirect_to and request: is_safe = is_safe_login_or_logout_redirect( redirect_to=redirect_to, request_host=request.get_host(), dot_client_id=None, require_https=request.is_secure(), ) if not is_safe: safe_redirect_url = getattr(settings, 'SOCIAL_AUTH_LOGIN_REDIRECT_URL', '/dashboard') strategy.session_set(REDIRECT_FIELD_NAME, safe_redirect_url)
def target(self): """ If a redirect_url is specified in the querystring for this request, and the value is a safe url for redirect, the view will redirect to this page after rendering the template. If it is not specified, we will use the default target url. """ target_url = self.request.GET.get('redirect_url') or self.request.GET.get('next') # Some third party apps do not build URLs correctly and send next query param without URL-encoding, resulting # all plus('+') signs interpreted as space(' ') in the process of URL-decoding # for example if we hit on: # >> http://example.com/logout?next=/courses/course-v1:ARTS+D1+2018_T/course/ # we will receive in request.GET['next'] # >> /courses/course-v1:ARTS D1 2018_T/course/ # instead of # >> /courses/course-v1:ARTS+D1+2018_T/course/ # to handle this scenario we need to encode our URL using quote_plus and then unquote it again. if target_url: target_url = urllib.unquote(urllib.quote_plus(target_url)) if target_url and is_safe_login_or_logout_redirect(self.request, target_url): return target_url else: return self.default_target
def _get_redirect_to(request_host, request_headers, request_params, request_is_https): """ Determine the redirect url and return if safe Arguments: request_host (str) request_headers (dict) request_params (QueryDict) request_is_https (bool) Returns: str redirect url if safe else None """ redirect_to = request_params.get('next') header_accept = request_headers.get('HTTP_ACCEPT', '') accepts_text_html = any(mime_type in header_accept for mime_type in {'*/*', 'text/*', 'text/html'}) # If we get a redirect parameter, make sure it's safe i.e. not redirecting outside our domain. # Also make sure that it is not redirecting to a static asset and redirected page is web page # not a static file. As allowing assets to be pointed to by "next" allows 3rd party sites to # get information about a user on edx.org. In any such case drop the parameter. if redirect_to: mime_type, _ = mimetypes.guess_type(redirect_to, strict=False) safe_redirect = is_safe_login_or_logout_redirect( redirect_to=redirect_to, request_host=request_host, dot_client_id=request_params.get('client_id'), require_https=request_is_https, ) if not safe_redirect: log.warning( u"Unsafe redirect parameter detected after login page: '%(redirect_to)s'", {"redirect_to": redirect_to}) redirect_to = None elif not accepts_text_html: log.info( u"Redirect to non html content '%(content_type)s' detected from '%(user_agent)s'" u" after login page: '%(redirect_to)s'", { "redirect_to": redirect_to, "content_type": header_accept, "user_agent": request_headers.get('HTTP_USER_AGENT', '') }) redirect_to = None elif mime_type: log.warning( u"Redirect to url path with specified filed type '%(mime_type)s' not allowed: '%(redirect_to)s'", { "redirect_to": redirect_to, "mime_type": mime_type }) redirect_to = None elif settings.STATIC_URL in redirect_to: log.warning( u"Redirect to static content detected after login page: '%(redirect_to)s'", {"redirect_to": redirect_to}) redirect_to = None else: themes = get_themes() next_path = urllib.parse.urlparse(redirect_to).path for theme in themes: if theme.theme_dir_name in next_path: log.warning( u"Redirect to theme content detected after login page: '%(redirect_to)s'", {"redirect_to": redirect_to}) redirect_to = None break return redirect_to
def test_safe_redirect(self, url, host, req_is_secure, expected_is_safe): """ Test safe next parameter """ req = self.request.get('/login', HTTP_HOST=host) req.is_secure = lambda: req_is_secure actual_is_safe = is_safe_login_or_logout_redirect(req, url) self.assertEqual(actual_is_safe, expected_is_safe)
def test_safe_redirect(self, url, host, expected_is_safe): """ Test safe next parameter """ req = self.request.get('/login', HTTP_HOST=host) actual_is_safe = is_safe_login_or_logout_redirect(req, url) self.assertEqual(actual_is_safe, expected_is_safe)