Weblate: Missing access control for the AddonViewSet API exposes all addon configurations
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.
| Package | Affected versions | Patched versions |
|---|---|---|
weblatePyPI | < 5.16.1 | 5.16.1 |
Affected products
1- Range: < 5.16.1
Patches
27802c9b121ebfix(api): restrict add-on listing for project members
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:
3f58f9a4152bfix(api): properly filter add-ons on listing
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- github.com/advisories/GHSA-wppc-7cq7-cgfvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27457ghsaADVISORY
- github.com/WeblateOrg/weblate/commit/3f58f9a4152bc0cbdd6eff5954f9c7bc4d9f0af9ghsax_refsource_MISCWEB
- github.com/WeblateOrg/weblate/commit/7802c9b121eb407c48d4adddd4f2458fb3efef0fghsax_refsource_MISCWEB
- github.com/WeblateOrg/weblate/pull/18107ghsax_refsource_MISCWEB
- github.com/WeblateOrg/weblate/pull/18164ghsax_refsource_MISCWEB
- github.com/WeblateOrg/weblate/releases/tag/weblate-5.16.1ghsax_refsource_MISCWEB
- github.com/WeblateOrg/weblate/security/advisories/GHSA-wppc-7cq7-cgfvghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.