VYPR
Moderate severityNVD Advisory· Published Feb 26, 2026· Updated Mar 3, 2026

Weblate: Missing access control for the AddonViewSet API exposes all addon configurations

CVE-2026-27457

Description

Weblate is a web based localization tool. Prior to version 5.16.1, the REST API's AddonViewSet (weblate/api/views.py, line 2831) uses queryset = Addon.objects.all() without overriding get_queryset() to scope results by user permissions. This allows any authenticated user (or anonymous users if REQUIRE_LOGIN is not set) to list and retrieve ALL addons across all projects and components via GET /api/addons/ and GET /api/addons/{id}/. Version 5.16.1 fixes the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
weblatePyPI
< 5.16.15.16.1

Affected products

1

Patches

2
7802c9b121eb

fix(api): restrict add-on listing for project members

https://github.com/WeblateOrg/weblateMichal ČihařFeb 23, 2026via ghsa
2 files changed · +19 8
  • weblate/api/tests.py+17 6 modified
    @@ -6137,7 +6137,7 @@ def test_delete(self) -> None:
                 "api:addon-detail",
                 kwargs={"pk": response.data["id"]},
                 method="delete",
    -            code=403,
    +            code=404,
             )
             self.do_request(
                 "api:addon-detail",
    @@ -6153,13 +6153,13 @@ def addon_scope_test(
             expect_access: bool,
             authenticated: bool,
             superuser: bool,
    -        add_user: bool = False,
    +        add_user: str = "",
         ) -> None:
             project = self.component.project
             project.access_control = Project.ACCESS_PRIVATE
             project.save(update_fields=["access_control"])
             if add_user:
    -            project.add_user(self.user, "Translate")
    +            project.add_user(self.user, add_user)
     
             addon_component = self.component.addon_set.create(
                 name="weblate.gettext.linguas"
    @@ -6206,7 +6206,18 @@ def test_access_user(self) -> None:
     
         def test_access_user_member(self) -> None:
             self.addon_scope_test(
    -            expect_access=True, authenticated=True, superuser=False, add_user=True
    +            expect_access=False,
    +            authenticated=True,
    +            superuser=False,
    +            add_user="Translate",
    +        )
    +
    +    def test_access_user_admin(self) -> None:
    +        self.addon_scope_test(
    +            expect_access=True,
    +            authenticated=True,
    +            superuser=False,
    +            add_user="Administration",
             )
     
         def test_configuration(self) -> None:
    @@ -6236,7 +6247,7 @@ def test_edit(self) -> None:
                 "api:addon-detail",
                 kwargs={"pk": response.data["id"]},
                 method="patch",
    -            code=403,
    +            code=404,
                 format="json",
                 request={"configuration": expected},
             )
    @@ -6287,7 +6298,7 @@ def test_delete_project_addon(self) -> None:
                 "api:addon-detail",
                 kwargs={"pk": response.data["id"]},
                 method="delete",
    -            code=403,
    +            code=404,
             )
             self.do_request(
                 "api:addon-detail",
    
  • weblate/api/views.py+2 2 modified
    @@ -2836,8 +2836,8 @@ def get_queryset(self):
             if self.request.user.has_perm("management.addons"):
                 return Addon.objects.order_by("id")
             return Addon.objects.filter(
    -            Q(project__in=self.request.user.allowed_projects)
    -            | Q(component__project__in=self.request.user.allowed_projects)
    +            Q(project__in=self.request.user.managed_projects)
    +            | Q(component__project__in=self.request.user.managed_projects)
             ).order_by("id")
     
         def perm_check(self, request: Request, instance: Addon) -> None:
    
3f58f9a4152b

fix(api): properly filter add-ons on listing

https://github.com/WeblateOrg/weblateMichal ČihařFeb 19, 2026via ghsa
3 files changed · +93 17
  • docs/changes.rst+1 0 modified
    @@ -12,6 +12,7 @@ Weblate 5.16.1
     .. rubric:: Bug fixes
     
     * :ref:`check-punctuation-spacing` better handles XML markup.
    +* Access control for add-ons listing in API.
     
     .. rubric:: Compatibility
     
    
  • weblate/api/tests.py+73 9 modified
    @@ -23,6 +23,7 @@
         NotificationFrequency,
         NotificationScope,
     )
    +from weblate.addons.models import Addon
     from weblate.api.serializers import CommentSerializer, RepoOperations
     from weblate.auth.models import (
         Group,
    @@ -129,7 +130,7 @@ def do_request(
             method="get",
             request=None,
             headers=None,
    -        skip=(),
    +        skip: set[str] | None = None,
             # pylint: disable-next=redefined-builtin
             format: str = "multipart",  # noqa: A002
         ):
    @@ -147,8 +148,9 @@ def do_request(
                 f"Unexpected status code {response.status_code}: {content}",
             )
             if data is not None:
    -            for item in skip:
    -                del response.data[item]
    +            if skip:
    +                for item in skip:
    +                    del response.data[item]
                 self.maxDiff = None
                 self.assertEqual(response.data, data)
             return response
    @@ -242,7 +244,7 @@ def test_get_anonymous(self) -> None:
                 superuser=False,
                 code=200,
                 data={"full_name": "Anonymous", "username": settings.ANONYMOUS_USER_NAME},
    -            skip=("id",),
    +            skip={"id"},
             )
             # Admin can get full details
             self.do_request(
    @@ -260,7 +262,7 @@ def test_get_anonymous(self) -> None:
                     "is_bot": False,
                     "last_login": None,
                 },
    -            skip=(
    +            skip={
                     "id",
                     "groups",
                     "languages",
    @@ -270,7 +272,7 @@ def test_get_anonymous(self) -> None:
                     "statistics_url",
                     "contributions_url",
                     "date_expires",
    -            ),
    +            },
             )
     
         def test_filter_superuser(self) -> None:
    @@ -1510,7 +1512,7 @@ def test_repo_status(self) -> None:
                         "eligible_for_commit": 0,
                     },
                 },
    -            skip=("url",),
    +            skip={"url"},
             )
     
         def test_components(self) -> None:
    @@ -3288,7 +3290,7 @@ def test_statistics(self) -> None:
                 "api:component-statistics",
                 self.component_kwargs,
                 data={"count": 4},
    -            skip=("results", "previous", "next"),
    +            skip={"results", "previous", "next"},
             )
             response = self.do_request(
                 "api:component-statistics",
    @@ -4283,7 +4285,7 @@ def test_statistics(self) -> None:
                     "readonly_words_percent": 0.0,
                     "readonly_chars_percent": 0.0,
                 },
    -            skip=("last_change",),
    +            skip={"last_change"},
             )
     
         def test_changes(self) -> None:
    @@ -6145,6 +6147,68 @@ def test_delete(self) -> None:
                 code=204,
             )
     
    +    def addon_scope_test(
    +        self,
    +        *,
    +        expect_access: bool,
    +        authenticated: bool,
    +        superuser: bool,
    +        add_user: bool = False,
    +    ) -> None:
    +        project = self.component.project
    +        project.access_control = Project.ACCESS_PRIVATE
    +        project.save(update_fields=["access_control"])
    +        if add_user:
    +            project.add_user(self.user, "Translate")
    +
    +        addon_component = self.component.addon_set.create(
    +            name="weblate.gettext.linguas"
    +        )
    +        addon_project = project.addon_set.create(name="weblate.gettext.linguas")
    +        addon_site = Addon.objects.create(name="weblate.gettext.linguas")
    +        self.do_request(
    +            "api:addon-list",
    +            superuser=superuser,
    +            authenticated=authenticated,
    +            data={"count": (3 if superuser else 2) if expect_access else 0},
    +            skip={"results", "previous", "next"},
    +        )
    +        self.do_request(
    +            "api:addon-detail",
    +            kwargs={"pk": addon_component.pk},
    +            superuser=superuser,
    +            authenticated=authenticated,
    +            code=200 if expect_access else 404,
    +        )
    +        self.do_request(
    +            "api:addon-detail",
    +            kwargs={"pk": addon_project.pk},
    +            superuser=superuser,
    +            authenticated=authenticated,
    +            code=200 if expect_access else 404,
    +        )
    +        self.do_request(
    +            "api:addon-detail",
    +            kwargs={"pk": addon_site.pk},
    +            superuser=superuser,
    +            authenticated=authenticated,
    +            code=200 if expect_access and superuser else 404,
    +        )
    +
    +    def test_access_anonymous(self) -> None:
    +        self.addon_scope_test(expect_access=False, authenticated=False, superuser=False)
    +
    +    def test_access_superuser(self) -> None:
    +        self.addon_scope_test(expect_access=True, authenticated=True, superuser=True)
    +
    +    def test_access_user(self) -> None:
    +        self.addon_scope_test(expect_access=False, authenticated=True, superuser=False)
    +
    +    def test_access_user_member(self) -> None:
    +        self.addon_scope_test(
    +            expect_access=True, authenticated=True, superuser=False, add_user=True
    +        )
    +
         def test_configuration(self) -> None:
             self.create_addon(
                 name="weblate.gettext.mo", configuration={"path": "{{var}}"}, code=400
    
  • weblate/api/views.py+19 8 modified
    @@ -2829,17 +2829,28 @@ def destroy(self, request: Request, pk=None):
         partial_update=extend_schema(description="Edit partial information about add-on."),
     )
     class AddonViewSet(viewsets.ReadOnlyModelViewSet, UpdateModelMixin, DestroyModelMixin):
    -    queryset = Addon.objects.all()
    +    queryset = Addon.objects.none()
         serializer_class = AddonSerializer
     
    +    def get_queryset(self):
    +        if self.request.user.has_perm("management.addons"):
    +            return Addon.objects.order_by("id")
    +        return Addon.objects.filter(
    +            Q(project__in=self.request.user.allowed_projects)
    +            | Q(component__project__in=self.request.user.allowed_projects)
    +        ).order_by("id")
    +
         def perm_check(self, request: Request, instance: Addon) -> None:
    -        if instance.component and not request.user.has_perm(
    -            "component.edit", instance.component
    -        ):
    -            self.permission_denied(request, "Can not manage addons")
    -        if instance.project and not request.user.has_perm(
    -            "project.edit", instance.project
    -        ):
    +        if instance.component:
    +            # Component-level add-ons
    +            if not request.user.has_perm("component.edit", instance.component):
    +                self.permission_denied(request, "Can not manage addons")
    +        elif instance.project:
    +            # Project-level add-ons
    +            if not request.user.has_perm("project.edit", instance.project):
    +                self.permission_denied(request, "Can not manage addons")
    +        elif not request.user.has_perm("management.addons"):
    +            # Site-wide add-ons
                 self.permission_denied(request, "Can not manage addons")
     
         def update(self, request: Request, *args, **kwargs):
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.