diff --git a/apps/authentication/api/sso.py b/apps/authentication/api/sso.py index 0756bad4e..6e48bda41 100644 --- a/apps/authentication/api/sso.py +++ b/apps/authentication/api/sso.py @@ -14,7 +14,7 @@ from common.api import JMSGenericViewSet from common.const.http import POST, GET from common.permissions import OnlySuperUser from common.serializers import EmptySerializer -from common.utils import reverse +from common.utils import reverse, safe_next_url from common.utils.timezone import utc_now from users.models import User from ..errors import SSOAuthClosed @@ -45,6 +45,7 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet): username = serializer.validated_data['username'] user = User.objects.get(username=username) next_url = serializer.validated_data.get(NEXT_URL) + next_url = safe_next_url(next_url, request=request) operator = request.user.username # TODO `created_by` 和 `created_by` 可以通过 `ThreadLocal` 统一处理 diff --git a/apps/authentication/backends/oidc/views.py b/apps/authentication/backends/oidc/views.py index 98bd2ef2a..56c1e4fb0 100644 --- a/apps/authentication/backends/oidc/views.py +++ b/apps/authentication/backends/oidc/views.py @@ -20,10 +20,11 @@ from django.core.exceptions import SuspiciousOperation from django.http import HttpResponseRedirect, QueryDict from django.urls import reverse from django.utils.crypto import get_random_string -from django.utils.http import url_has_allowed_host_and_scheme, urlencode +from django.utils.http import urlencode from django.views.generic import View from authentication.utils import build_absolute_uri_for_oidc +from common.utils import safe_next_url from .utils import get_logger logger = get_logger(__file__) @@ -100,8 +101,7 @@ class OIDCAuthRequestView(View): # Stores the "next" URL in the session if applicable. logger.debug(log_prompt.format('Stores next url in the session')) next_url = request.GET.get('next') - request.session['oidc_auth_next_url'] = next_url \ - if url_has_allowed_host_and_scheme(url=next_url, allowed_hosts=(request.get_host(),)) else None + request.session['oidc_auth_next_url'] = safe_next_url(next_url, request=request) # Redirects the user to authorization endpoint. logger.debug(log_prompt.format('Construct redirect url')) diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index 536ef4155..9b7f81e25 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -18,7 +18,7 @@ from authentication.permissions import UserConfirmation from common.sdk.im.dingtalk import URL, DingTalk from common.utils import get_logger from common.utils.common import get_request_ip -from common.utils.django import get_object_or_none, reverse +from common.utils.django import get_object_or_none, reverse, safe_next_url from common.utils.random import random_string from common.views.mixins import PermissionsMixin, UserConfirmRequiredExceptionMixin from users.models import User @@ -185,6 +185,7 @@ class DingTalkQRLoginView(DingTalkQRMixin, METAMixin, View): def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') or reverse('index') next_url = self.get_next_url_from_meta() or reverse('index') + next_url = safe_next_url(next_url, request=request) redirect_uri = reverse('authentication:dingtalk-qr-login-callback', external=True) redirect_uri += '?' + urlencode({ diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 6514e14eb..0b88a76d8 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -24,7 +24,7 @@ from django.views.decorators.debug import sensitive_post_parameters from django.views.generic.base import TemplateView, RedirectView from django.views.generic.edit import FormView -from common.utils import FlashMessageUtil, static_or_direct +from common.utils import FlashMessageUtil, static_or_direct, safe_next_url from users.utils import ( redirect_user_first_login_or_index ) @@ -202,6 +202,7 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView): auth_name, redirect_url = auth_method['name'], auth_method['url'] next_url = request.GET.get('next') or '/' + next_url = safe_next_url(next_url, request=request) query_string = request.GET.urlencode() redirect_url = '{}?next={}&{}'.format(redirect_url, next_url, query_string) diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py index 6817c84a0..2e6d0c1bb 100644 --- a/apps/authentication/views/wecom.py +++ b/apps/authentication/views/wecom.py @@ -19,7 +19,7 @@ from common.sdk.im.wecom import URL from common.sdk.im.wecom import WeCom from common.utils import get_logger from common.utils.common import get_request_ip -from common.utils.django import reverse, get_object_or_none +from common.utils.django import reverse, get_object_or_none, safe_next_url from common.utils.random import random_string from common.views.mixins import UserConfirmRequiredExceptionMixin, PermissionsMixin from users.models import User @@ -182,6 +182,7 @@ class WeComQRLoginView(WeComQRMixin, METAMixin, View): def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') or reverse('index') next_url = self.get_next_url_from_meta() or reverse('index') + next_url = safe_next_url(next_url, request=request) redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True) redirect_uri += '?' + urlencode({ 'redirect_url': redirect_url, diff --git a/apps/common/utils/django.py b/apps/common/utils/django.py index ad8a218d8..fc692bc15 100644 --- a/apps/common/utils/django.py +++ b/apps/common/utils/django.py @@ -7,6 +7,7 @@ from django.db import models from django.db.models.signals import post_save, pre_save from django.shortcuts import reverse as dj_reverse from django.utils import timezone +from django.utils.http import url_has_allowed_host_and_scheme UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}') @@ -94,3 +95,12 @@ def get_request_os(request): return 'linux' else: return 'unknown' + + +def safe_next_url(next_url, request=None): + safe_hosts = [*settings.ALLOWED_HOSTS] + if request: + safe_hosts.append(request.get_host()) + if not next_url or not url_has_allowed_host_and_scheme(next_url, safe_hosts): + next_url = '/' + return next_url diff --git a/apps/users/utils.py b/apps/users/utils.py index 29ce56cd1..6db4943de 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -11,7 +11,7 @@ from django.conf import settings from django.core.cache import cache from common.tasks import send_mail_async -from common.utils import reverse, get_object_or_none, ip +from common.utils import reverse, get_object_or_none, ip, safe_next_url from .models import User logger = logging.getLogger('jumpserver.users') @@ -49,6 +49,7 @@ def redirect_user_first_login_or_index(request, redirect_field_name): url = request.POST.get(redirect_field_name) if not url: url = request.GET.get(redirect_field_name) + url = safe_next_url(url, request=request) # 防止 next 地址为 None if not url or url.lower() in ['none']: url = reverse('index') diff --git a/apps/users/views/profile/password.py b/apps/users/views/profile/password.py index 87d0bf6e7..8f345db93 100644 --- a/apps/users/views/profile/password.py +++ b/apps/users/views/profile/password.py @@ -30,7 +30,7 @@ class UserVerifyPasswordView(AuthMixin, FormView): try: password = form.cleaned_data['password'] except errors.AuthFailedError as e: - form.add_error("password", _(f"Password invalid") + f'({e.msg})') + form.add_error("password", _("Password invalid") + f'({e.msg})') return self.form_invalid(form) user = authenticate(request=self.request, username=user.username, password=password)