Medium severity4.3NVD Advisory· Published May 7, 2026· Updated May 11, 2026
CVE-2026-44263
CVE-2026-44263
Description
Weblate is a web based localization tool. Prior to version 5.17.1, the screenshots, tasks, and component link API allowed for the enumeration of translations in a project inaccessible to the user. This issue has been patched in version 5.17.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
weblatePyPI | < 5.17.1 | 5.17.1 |
Affected products
1Patches
16cf892c7bd50fix(api): use user accessible filter for params
3 files changed · +115 −17
weblate/api/serializers.py+1 −4 modified@@ -1677,13 +1677,10 @@ def validate(self, attrs): category_id = attrs.get("category_id") if category_id is not None: try: - category = Category.objects.get(pk=category_id) + category = project.category_set.get(pk=category_id) except Category.DoesNotExist as error: msg = "Category not found." raise serializers.ValidationError({"category_id": msg}) from error - if category.project != project: - msg = "The category does not belong to the selected project." - raise serializers.ValidationError({"category_id": msg}) attrs["category"] = category else: attrs["category"] = None
weblate/api/tests.py+95 −2 modified@@ -5883,14 +5883,26 @@ def test_links_with_wrong_project_category(self) -> None: def test_links_with_invalid_category(self) -> None: """Non-existent category_id should be rejected.""" self.create_acl() - self.do_request( + missing_response = self.do_request( "api:component-links", self.component_kwargs, method="post", code=400, superuser=True, request={"project_slug": "acl", "category_id": 99999}, ) + category = Category.objects.create( + name="Wrong Category", slug="wrong-cat", project=self.component.project + ) + wrong_project_response = self.do_request( + "api:component-links", + self.component_kwargs, + method="post", + code=400, + superuser=True, + request={"project_slug": "acl", "category_id": category.pk}, + ) + self.assertEqual(wrong_project_response.data, missing_response.data) def test_links_duplicate(self) -> None: """Adding a link to an already linked project should return 400.""" @@ -6143,7 +6155,7 @@ def ready(self): {"completed": False, "progress": 0, "result": None, "log": ""}, ) - def test_retrieve_denies_inaccessible_cached_component(self) -> None: + def test_retrieve_hides_inaccessible_cached_component(self) -> None: other_component = self.create_acl() cache.set( get_task_metadata_key(self.task_id), @@ -6165,6 +6177,60 @@ def ready(self): "api:task-detail", kwargs={"pk": self.task_id}, method="get", + code=404, + ) + + def test_retrieve_hides_inaccessible_cached_translation(self) -> None: + other_component = self.create_acl() + cache.set( + get_task_metadata_key(self.task_id), + { + "component_id": None, + "translation_id": other_component.source_translation.id, + }, + 3600, + ) + + class DummyAsyncResult: + def __init__(self, task_id): + self.id = task_id + self.result = None + self.state = "PENDING" + + def ready(self): + return False + + with patch("weblate.api.views.AsyncResult", DummyAsyncResult): + self.do_request( + "api:task-detail", + kwargs={"pk": self.task_id}, + method="get", + code=404, + ) + + def test_destroy_denies_visible_cached_component_without_edit_permission( + self, + ) -> None: + cache.set( + get_task_metadata_key(self.task_id), + {"component_id": self.component.id, "translation_id": None}, + 3600, + ) + + class DummyAsyncResult: + def __init__(self, task_id): + self.id = task_id + self.result = None + self.state = "PENDING" + + def ready(self): + return False + + with patch("weblate.api.views.AsyncResult", DummyAsyncResult): + self.do_request( + "api:task-detail", + kwargs={"pk": self.task_id}, + method="delete", code=403, ) @@ -9059,6 +9125,33 @@ def test_create(self) -> None: 1, ) + def test_create_hides_inaccessible_translation(self) -> None: + private_component = self.create_acl() + hidden_response = self.do_request( + "api:screenshot-list", + method="post", + code=400, + request={ + "name": "Hidden translation screenshot", + "project_slug": private_component.project.slug, + "component_slug": private_component.slug, + "language_code": private_component.source_translation.language.code, + }, + ) + missing_response = self.do_request( + "api:screenshot-list", + method="post", + code=400, + request={ + "name": "Missing translation screenshot", + "project_slug": "missing", + "component_slug": private_component.slug, + "language_code": private_component.source_translation.language.code, + }, + ) + + self.assertEqual(hidden_response.data, missing_response.data) + def test_patch_screenshot(self) -> None: self.do_request( "api:screenshot-detail",
weblate/api/views.py+19 −11 modified@@ -3206,13 +3206,14 @@ def delete_units(self, request: Request, pk, unit_id): def create(self, request: Request, *args, **kwargs): """Create a new screenshot.""" + user = cast("User", request.user) required_params = ["project_slug", "component_slug", "language_code"] for param in required_params: if param not in request.data: raise ValidationError({param: "This field is required."}) try: - translation = Translation.objects.get( + translation = Translation.objects.filter_access(user).get( component__project__slug=request.data["project_slug"], component__slug=request.data["component_slug"], language__code=request.data["language_code"], @@ -3222,26 +3223,26 @@ def create(self, request: Request, *args, **kwargs): dict.fromkeys(required_params, "Translation not found.") ) from error - if not request.user.has_perm("screenshot.add", translation): + if not user.has_perm("screenshot.add", translation): self.permission_denied(request, "Can not add screenshot.") with transaction.atomic(): serializer = ScreenshotCreateSerializer( data=request.data, context={"request": request} ) serializer.is_valid(raise_exception=True) - instance = serializer.save(translation=translation, user=request.user) + instance = serializer.save(translation=translation, user=user) instance.change_set.create( action=ActionEvents.SCREENSHOT_UPLOADED, - user=request.user, + user=user, target=instance.name, ) for unit in instance.units.all(): instance.change_set.create( action=ActionEvents.SCREENSHOT_ADDED, - user=request.user, + user=user, target=instance.name, unit=unit, ) @@ -3579,22 +3580,29 @@ def get_task( ) -> tuple[AsyncResult, Component | None]: obj: Model component: Component - task = AsyncResult(str(pk)) + user = cast("User", request.user) + task: AsyncResult = AsyncResult(str(pk)) metadata = get_task_metadata(str(pk)) or {} if translation_id := metadata.get("translation_id"): - obj = get_object_or_404(Translation, pk=translation_id) - component = obj.component + translation = get_object_or_404( + Translation.objects.filter_access(user), pk=translation_id + ) + obj = translation + component = translation.component elif component_id := metadata.get("component_id"): - component = obj = get_object_or_404(Component, pk=component_id) + component = get_object_or_404( + Component.objects.filter_access(user), pk=component_id + ) + obj = component else: msg = "Invalid task" raise Http404(msg) # Check access or permission if permission: - if not request.user.has_perm(permission, obj): + if not user.has_perm(permission, obj): raise PermissionDenied - elif not request.user.can_access_component(component): + elif not user.can_access_component(component): raise PermissionDenied return task, component
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
6- github.com/WeblateOrg/weblate/commit/6cf892c7bd50b667a65a99d716a90694f7d9f203nvdPatchWEB
- github.com/WeblateOrg/weblate/pull/19258nvdIssue TrackingPatchWEB
- github.com/WeblateOrg/weblate/security/advisories/GHSA-gcg5-86jr-f7jgnvdPatchVendor AdvisoryWEB
- github.com/advisories/GHSA-gcg5-86jr-f7jgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-44263ghsaADVISORY
- github.com/WeblateOrg/weblate/releases/tag/weblate-5.17.1nvdRelease NotesWEB
News mentions
0No linked articles in our index yet.