CVE-2023-22903: Other (NVD-CWE-Other)
value: api/views/user.py in LibrePhotos before e19e539 has incorrect access control.
CWE description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
CVSS v3 base score: 9.8
Published: 2023-01-10T06:15Z
Repository: https://github.com/LibrePhotos/librephotos
Language: Python
Files changed: api/views/user.py, ownphotos/urls.py
Commit date: 2023-01-09 00:37:41 +0100
Commit message: Fix incorrect access controll

Diff: diff --git a/api/views/user.py b/api/views/user.py
index a062cb7..3857028 100644
--- a/api/views/user.py
+++ b/api/views/user.py
@@ -1,7 +1,7 @@
 from rest_framework import permissions, status, viewsets
-from rest_framework.permissions import AllowAny, IsAdminUser
+from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated
 from rest_framework.response import Response
 from rest_framework.views import APIView
 
 import ownphotos.settings
 from api.api_util import path_to_dict
@@ -41,10 +41,19 @@ class RootPathTreeView(APIView):
             return Response(res)
         except Exception as e:
             logger.exception(str(e))
             return Response({"message": str(e)})
 
+class IsFirstTimeSetupView(APIView):
+    permission_classes = (AllowAny,)
+
+    def get(self, request, format=None):
+        try:
+            return Response({"isFirstTimeSetup": not User.objects.filter(is_superuser=True).exists()})
+        except Exception as e:
+            logger.exception(str(e))
+            return Response({"message": str(e)})
 
 class FirstTimeSetupPermission(permissions.BasePermission):
     message = "Check if the first time setup is done"
 
     def has_permission(self, request, view):
@@ -53,11 +62,11 @@ class FirstTimeSetupPermission(permissions.BasePermission):
 
 class UserViewSet(viewsets.ModelViewSet):
 
     serializer_class = UserSerializer
 
-    permission_classes = (IsUserOrReadOnly,)
+    permission_classes = (IsAdminUser,)
 
     def get_queryset(self):
         queryset = (
             User.objects.exclude(is_active=False)
             .only(
@@ -90,18 +99,19 @@ class UserViewSet(viewsets.ModelViewSet):
     def get_permissions(self):
         if self.action == "create":
             self.permission_classes = [
                 IsRegistrationAllowed | FirstTimeSetupPermission | IsAdminUser
             ]
-        elif self.action == "list":
-            self.permission_classes = (AllowAny,)
-        elif self.request.method == "GET" or self.request.method == "POST":
+        if self.request.method == "POST":
             self.permission_classes = (AllowAny,)
-        else:
-            self.permission_classes = (IsUserOrReadOnly,)
         return super(UserViewSet, self).get_permissions()
 
+    def create(self, request, *args, **kwargs):
+        if User.objects.filter(is_superuser=True).exists() and not request.user.is_superuser:
+            return Response(status=status.HTTP_401_UNAUTHORIZED)
+        return super(UserViewSet, self).create(request, *args, **kwargs)
+
     def retrieve(self, *args, **kwargs):
         return super(UserViewSet, self).retrieve(*args, **kwargs)
 
     def list(self, *args, **kwargs):
         return super(UserViewSet, self).list(*args, **kwargs)
diff --git a/ownphotos/urls.py b/ownphotos/urls.py
index 78ed689..3367647 100644
--- a/ownphotos/urls.py
+++ b/ownphotos/urls.py
@@ -191,10 +191,11 @@ router.register(r"api/exists", upload.UploadPhotoExists, basename="exists")
 router.register(r"api/jobs", jobs.LongRunningJobViewSet)
 urlpatterns = [
     url(r"^", include(router.urls)),
     url(r"^admin/", admin.site.urls),
     url(r"^api/sitesettings", views.SiteSettingsView.as_view()),
+    url(r"^api/firsttimesetup", user.IsFirstTimeSetupView.as_view()),
     url(r"^api/dirtree", user.RootPathTreeView.as_view()),
     url(r"^api/labelfaces", faces.SetFacePersonLabel.as_view()),
     url(r"^api/deletefaces", faces.DeleteFaces.as_view()),
     url(r"^api/photosedit/delete", photos.DeletePhotos.as_view()),
     url(r"^api/photosedit/duplicate/delete", photos.DeleteDuplicatePhotos.as_view()),

Vulnerable code: from rest_framework.permissions import AllowAny, IsAdminUser
    permission_classes = (IsUserOrReadOnly,)
        elif self.action == "list":
            self.permission_classes = (AllowAny,)
        elif self.request.method == "GET" or self.request.method == "POST":
        else:
            self.permission_classes = (IsUserOrReadOnly,)
Fixed code: from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated
class IsFirstTimeSetupView(APIView):
    permission_classes = (AllowAny,)

    def get(self, request, format=None):
        try:
            return Response({"isFirstTimeSetup": not User.objects.filter(is_superuser=True).exists()})
        except Exception as e:
            logger.exception(str(e))
            return Response({"message": str(e)})
    permission_classes = (IsAdminUser,)
        if self.request.method == "POST":
    def create(self, request, *args, **kwargs):
        if User.objects.filter(is_superuser=True).exists() and not request.user.is_superuser:
            return Response(status=status.HTTP_401_UNAUTHORIZED)
        return super(UserViewSet, self).create(request, *args, **kwargs)

    url(r"^api/firsttimesetup", user.IsFirstTimeSetupView.as_view()),