VYPR
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.

PackageAffected versionsPatched versions
weblatePyPI
< 5.17.15.17.1

Affected products

1
  • cpe:2.3:a:weblate:weblate:*:*:*:*:*:*:*:*
    Range: <5.17.1

Patches

1
6cf892c7bd50

fix(api): use user accessible filter for params

https://github.com/WeblateOrg/weblateMichal ČihařApr 27, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.